From 536653778e2e3562356ae59564a072681ef44437 Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 23:08:45 +0530 Subject: [PATCH 1/7] Update run-all-services.sh --- code/chapter11/run-all-services.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code/chapter11/run-all-services.sh b/code/chapter11/run-all-services.sh index 51277204..6ee8091b 100755 --- a/code/chapter11/run-all-services.sh +++ b/code/chapter11/run-all-services.sh @@ -27,10 +27,10 @@ echo "Starting all services with Docker Compose..." docker-compose up -d echo "All services are running:" -echo "- User Service: https://scaling-pancake-77vj4pwq7fpjqx-6050.app.github.dev/user" -echo "- Inventory Service: https://scaling-pancake-77vj4pwq7fpjqx-7050.app.github.dev/inventory" -echo "- Order Service: https://scaling-pancake-77vj4pwq7fpjqx-8050.app.github.dev/order" -echo "- Catalog Service: https://scaling-pancake-77vj4pwq7fpjqx-5050.app.github.dev/catalog" -echo "- Payment Service: https://scaling-pancake-77vj4pwq7fpjqx-9050.app.github.dev/payment" -echo "- Shopping Cart Service: https://scaling-pancake-77vj4pwq7fpjqx-4050.app.github.dev/shoppingcart" -echo "- Shipment Service: https://scaling-pancake-77vj4pwq7fpjqx-8060.app.github.dev/shipment" +echo "- User Service: https:///user" +echo "- Inventory Service: https:///inventory" +echo "- Order Service: https:///order" +echo "- Catalog Service: https:///catalog" +echo "- Payment Service: https:///payment" +echo "- Shopping Cart Service: https:///shoppingcart" +echo "- Shipment Service: https:///shipment" From 90ec6698d95236d4e1891867c5a52f5e8fb1f229 Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 23:09:25 +0530 Subject: [PATCH 2/7] Update docker-compose.yml --- code/chapter11/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/code/chapter11/docker-compose.yml b/code/chapter11/docker-compose.yml index bc6ba422..c52b6af0 100644 --- a/code/chapter11/docker-compose.yml +++ b/code/chapter11/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: user-service: build: ./user From 12dd517394d699a5b0074b3e2e5718ad1177654d Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 23:21:36 +0530 Subject: [PATCH 3/7] Update README.adoc --- code/chapter11/README.adoc | 829 +++++++++++++++++++++++++++++++++++-- 1 file changed, 804 insertions(+), 25 deletions(-) diff --git a/code/chapter11/README.adoc b/code/chapter11/README.adoc index ce07afb5..593f1c90 100644 --- a/code/chapter11/README.adoc +++ b/code/chapter11/README.adoc @@ -7,15 +7,8 @@ == Overview -This project demonstrates a microservices-based e-commerce application built with Jakarta EE and MicroProfile, running on Open Liberty runtime. The application is composed of multiple independent services that work together to provide a complete e-commerce solution. - -[.lead] -A practical demonstration of MicroProfile capabilities for building cloud-native Java microservices. - -[IMPORTANT] -==== -This project is part of the official MicroProfile API Tutorial. -==== +This project demonstrates a microservices-based e-commerce application built with Jakarta EE and MicroProfile, running on Open Liberty runtime. +The application is composed of multiple independent services that work together to provide a complete e-commerce solution. == Services @@ -44,13 +37,14 @@ The application is split into the following microservices: |Handles shipping orders, tracking, and delivery status updates |Payment Service -|Processes payments and manages payment methods and transactions +|Processes payments and manages payment methods and transactions. Demonstrates MicroProfile Rest Client 4.0 features including CDI injection, ResponseExceptionMapper, custom filters, and programmatic client creation. |=== == Technology Stack * *Jakarta EE 10.0*: For enterprise Java standardization -* *MicroProfile 6.1*: For cloud-native APIs +* *MicroProfile 7.x*: For cloud-native APIs +* *MicroProfile Rest Client 4.0*: Type-safe REST service consumption * *Open Liberty*: Lightweight, flexible runtime for Java microservices * *Maven*: For project management and builds @@ -58,8 +52,8 @@ The application is split into the following microservices: === Prerequisites -* JDK 17 or later -* Maven 3.6 or later +* JDK 21 or later +* Maven 3.9 or later * Docker (optional for containerized deployment) === Running the Application @@ -68,8 +62,8 @@ The application is split into the following microservices: + [source,bash] ---- -git clone https://github.com/your-username/liberty-rest-app.git -cd liberty-rest-app +git clone https://github.com/microprofile/microprofile-tutorial.git +cd code/chapter11 ---- 2. Start each microservice individually: @@ -158,6 +152,61 @@ Each microservice provides its own OpenAPI documentation, available at: * Order Service: http://localhost:8050/order/openapi * Catalog Service: http://localhost:9050/catalog/openapi +== MicroProfile Rest Client 4.0 Features + +This application demonstrates the latest MicroProfile Rest Client 4.0 features: + +=== New `baseUri(String)` Convenience Method + +The Inventory Service showcases the new `baseUri(String)` method that eliminates the need for `URI.create()`: + +[source,java] +---- +// MicroProfile Rest Client 4.0 - Simplified approach +ProductServiceClient client = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") // Direct String parameter + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(ProductServiceClient.class); +---- + +This replaces the older pattern: + +[source,java] +---- +// Old approach (Rest Client 3.x) +URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); +ProductServiceClient client = RestClientBuilder.newBuilder() + .baseUri(catalogServiceUri) // Required URI object + .build(ProductServiceClient.class); +---- + +=== CDI Integration with `@RestClient` + +The `@RestClient` qualifier is mandatory for CDI injection: + +[source,java] +---- +@Inject +@RestClient // Required qualifier +private ProductServiceClient productClient; +---- + +=== AutoCloseable Support + +REST client interfaces extend `AutoCloseable` for proper resource management: + +[source,java] +---- +@RegisterRestClient(configKey = "product-service") +@Path("/products") +public interface ProductServiceClient extends AutoCloseable { + // Client methods +} +---- + +See the `InventoryService` class for complete working examples of these features. + == Testing the Services === User Service @@ -287,6 +336,746 @@ curl -X GET http://localhost:9050/catalog/api/products/1 curl -X GET "http://localhost:9050/catalog/api/products/search?keyword=laptop" ---- +== Verifying MicroProfile Rest Client Examples + +The Payment Service demonstrates comprehensive MicroProfile Rest Client 4.0 features. Follow these steps to verify each example. + +=== Prerequisites + +Before testing the examples, ensure both services are running: + +[source,bash] +---- +# Terminal 1 - Start the Catalog Service (provides product data) +cd catalog +mvn liberty:run + +# Terminal 2 - Start the Payment Service (consumes catalog service) +cd payment +mvn liberty:run +---- + +The services will be available at: + +* Catalog Service: http://localhost:5050/catalog +* Payment Service: http://localhost:9050/payment + +=== Example 1: Basic REST Client with @RegisterRestClient + +This example demonstrates defining a MicroProfile Rest Client interface with `@RegisterRestClient`. + +**What to Verify:** + +* `ProductClient` interface is annotated with `@RegisterRestClient(configKey = "catalog-service")` +* The client uses Jakarta REST annotations (`@GET`, `@Path`, `@PathParam`) +* Configuration is externalized in `microprofile-config.properties` + +**Test the Client:** + +[source,bash] +---- +# Get all products via REST client +curl http://localhost:9050/payment/catalog/products + +# Expected Response: JSON array of products +[ + { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + ... +] +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` +* `src/main/resources/META-INF/microprofile-config.properties` + +=== Example 2: MicroProfile Config Integration + +This example shows externalized configuration for REST clients using MicroProfile Config. + +**What to Verify:** + +* Configuration properties follow the pattern: `/mp-rest/` +* Base URL, timeouts, and scope are configured +* Properties can be overridden via environment variables + +**Configuration Properties:** + +[source,properties] +---- +# Base URL +catalog-service/mp-rest/url=http://localhost:5050/catalog/api + +# Timeouts (in milliseconds) +catalog-service/mp-rest/connectTimeout=3000 +catalog-service/mp-rest/readTimeout=5000 + +# CDI Scope +catalog-service/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped + +# Follow redirects +catalog-service/mp-rest/followRedirects=true +---- + +**Test Configuration:** + +[source,bash] +---- +# Verify the client respects timeout settings +# (This will timeout if catalog service is slow) +time curl http://localhost:9050/payment/catalog/products +---- + +**Files to Review:** + +* `src/main/resources/META-INF/microprofile-config.properties` + +=== Example 3: CDI Injection with @Inject and @RestClient + +This example demonstrates dependency injection of REST clients using CDI. + +**What to Verify:** + +* `ProductClient` is injected with `@Inject` and `@RestClient` qualifiers +* The `@RestClient` qualifier is mandatory in MicroProfile Rest Client 4.0 +* Service layer (`ProductCatalogService`) uses the injected client +* `@ApplicationScoped` bean lifecycle management + +**Test CDI Injection:** + +[source,bash] +---- +# Test service that uses injected REST client +curl http://localhost:9050/payment/catalog/products + +# Get specific product (uses getProductById method) +curl http://localhost:9050/payment/catalog/products/1 + +# Expected Response: +{ + "id": 1, + "name": "Laptop", + "price": 999.99 +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java` +* `src/main/webapp/WEB-INF/beans.xml` (CDI configuration) + +=== Example 4: Parameter Annotations (@PathParam, @QueryParam) + +This example shows different parameter types in REST client methods. + +**What to Verify:** + +* `@PathParam` for path variables (e.g., `/products/{id}`) +* `@QueryParam` for query string parameters (e.g., `?price=99.99`) +* Parameters are automatically encoded and passed to remote service + +**Test Path Parameters:** + +[source,bash] +---- +# Test @PathParam - Get product by ID +curl http://localhost:9050/payment/catalog/products/1 + +# Test with different IDs +curl http://localhost:9050/payment/catalog/products/2 +curl http://localhost:9050/payment/catalog/products/3 +---- + +**Test Query Parameters:** + +[source,bash] +---- +# Test @QueryParam - Validate product price +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" + +# Expected Response (if price matches): +{ + "productId": 1, + "expectedPrice": 999.99, + "valid": true, + "message": "Price matches" +} + +# Test with wrong price +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=500.00" + +# Expected Response (if price doesn't match): +{ + "productId": 1, + "expectedPrice": 500.0, + "valid": false, + "message": "Price mismatch detected" +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (method signatures) +* `src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java` (endpoint implementation) + +=== Example 5: ResponseExceptionMapper - Custom Error Handling + +This example demonstrates mapping HTTP error responses to custom exceptions. + +**What to Verify:** + +* `ProductServiceResponseExceptionMapper` implements `ResponseExceptionMapper` +* HTTP 404 responses map to `ProductNotFoundException` (checked exception) +* HTTP 503 responses map to `ServiceUnavailableException` (unchecked exception) +* Error messages are extracted from JSON response bodies +* `@Priority` annotation controls mapper ordering + +**Test Exception Mapping:** + +[source,bash] +---- +# Test successful request (no exception) +curl http://localhost:9050/payment/catalog/products/1/detailed + +# Expected Response: +{ + "productId": 1, + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "note": "Product retrieved successfully - no exceptions thrown" +} + +# Test 404 error (ProductNotFoundException) +curl http://localhost:9050/payment/catalog/products/999999/detailed + +# Expected Response: +{ + "productId": 999999, + "error": "Product not found", + "note": "ProductNotFoundException was caught and handled by the service layer" +} + +# Test product availability (handles 404 gracefully) +curl http://localhost:9050/payment/catalog/products/999999/availability + +# Expected Response: +{ + "productId": 999999, + "available": false, + "message": "Product is not available" +} +---- + +**Verify Exception Types:** + +1. **Checked Exception (ProductNotFoundException)**: + - Must be declared in method signature: `throws ProductNotFoundException` + - Only thrown if method declares it + - Handled gracefully in service layer + +2. **Unchecked Exception (ServiceUnavailableException)**: + - Extends `RuntimeException` + - Always thrown regardless of method signature + - Used for service availability errors + +**Test Service Unavailable Scenario:** + +[source,bash] +---- +# Stop the catalog service to simulate 503 error +# Then try to access products +curl http://localhost:9050/payment/catalog/products/1/detailed + +# Expected Response (if catalog service is down): +{ + "productId": 1, + "error": "Catalog service unavailable", + "statusCode": 503, + "message": "...", + "note": "ServiceUnavailableException (unchecked) was thrown by ResponseExceptionMapper" +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java` +* `src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java` +* `src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java` +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (see `@RegisterProvider`) + +=== Verifying beans.xml Configuration + +The `beans.xml` file is required for CDI bean discovery and REST client registration. + +**What to Verify:** + +* File location: `src/main/webapp/WEB-INF/beans.xml` +* Bean discovery mode: `bean-discovery-mode="all"` +* Version: Jakarta EE 10 (beans 4.0) + +[source,xml] +---- + + + +---- + +**Files to Review:** + +* `src/main/webapp/WEB-INF/beans.xml` + +=== Additional Testing Scenarios + +==== Test Complete Product Validation Flow + +[source,bash] +---- +# 1. Check if product is available +curl http://localhost:9050/payment/catalog/products/1/availability + +# 2. Validate product price before payment +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" + +# 3. Get product details for payment processing +curl http://localhost:9050/payment/catalog/products/1 +---- + +==== Test Error Scenarios + +[source,bash] +---- +# Invalid product ID (404) +curl http://localhost:9050/payment/catalog/products/999999 + +# Missing query parameter (400) +curl http://localhost:9050/payment/catalog/products/1/validate-price + +# Invalid price value (400) +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=-10" +---- + +=== Viewing OpenAPI Documentation + +Access the OpenAPI documentation to see all available endpoints: + +[source,bash] +---- +# Payment Service OpenAPI documentation +open http://localhost:9050/openapi/ui/ + +# Or view the OpenAPI JSON spec +curl http://localhost:9050/openapi +---- + +=== Debugging Tips + +**Enable Detailed Logging:** + +Edit `src/main/liberty/config/server.xml` and add: + +[source,xml] +---- + +---- + +**Check Server Logs:** + +[source,bash] +---- +# View real-time logs +tail -f target/liberty/wlp/usr/servers/mpServer/logs/messages.log + +# Search for REST client activity +grep "ProductClient" target/liberty/wlp/usr/servers/mpServer/logs/messages.log +---- + +**Verify Configuration Loading:** + +[source,bash] +---- +# Check if properties are loaded correctly +# Look for log messages about MicroProfile Config +grep "mp-rest" target/liberty/wlp/usr/servers/mpServer/logs/messages.log +---- + +=== Example 6: Custom Filters and Interceptors + +This example demonstrates implementing custom filters for cross-cutting concerns like authentication, logging, and distributed tracing. + +**What to Verify:** + +* `ClientRequestFilter` implementations for request interception +* `ClientResponseFilter` implementations for response logging +* Filter registration using `@RegisterProvider` +* Filter execution order using `@Priority` +* Cross-cutting concerns separated from business logic + +**Filters Implemented:** + +1. **BearerTokenFilter** (Priority 1000 - AUTHENTICATION) + - Adds Authorization header with Bearer token + - Reads token from MicroProfile Config + - Runs first due to authentication priority + +2. **CorrelationIdFilter** (Priority 100) + - Adds X-Correlation-ID for distributed tracing + - Generates or propagates correlation IDs + - Adds unique X-Request-ID for each request + +3. **RequestLoggingFilter** (Priority 300) + - Logs complete outgoing request details + - Masks sensitive headers (Authorization, API keys) + - Runs after authentication and correlation filters + +4. **ResponseLoggingFilter** (Priority 300) + - Logs complete incoming response details + - Includes status codes, headers, timing + - Correlates responses with requests + +**Test Basic Filter Functionality:** + +[source,bash] +---- +# Get all products with filters (watch server logs) +curl http://localhost:9050/payment/catalog/filtered/products + +# Expected in logs: +# 1. "Bearer token authentication added to request" (if token configured) +# 2. "Generated new Correlation ID: " +# 3. "=== Outgoing REST Client Request ===" +# 4. "Method: GET" +# 5. "URI: http://localhost:5050/catalog/api/products" +# 6. "=== Incoming REST Client Response ===" +# 7. "Status: 200 OK" +---- + +**Test Filter Execution with Specific Product:** + +[source,bash] +---- +# Get specific product - observe filter chain execution +curl http://localhost:9050/payment/catalog/filtered/products/1 + +# Expected Response: +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "method": "REST Client with Custom Filters", + "filterChain": "BearerToken → CorrelationId → RequestLogging → [HTTP] → ResponseLogging", + "note": "Check server logs for X-Correlation-ID and detailed request/response traces" +} +---- + +**Test Filters with Error Responses:** + +[source,bash] +---- +# Test 404 error - filters still execute and log the error +curl http://localhost:9050/payment/catalog/filtered/products/999999 + +# Expected in logs: +# - RequestLoggingFilter logs the outgoing request +# - ResponseLoggingFilter logs: "Status: 404 Not Found" +# - ResponseLoggingFilter logs: "Result: CLIENT ERROR (404)" +# - ProductNotFoundException is thrown after logging + +# Check product availability (handles 404 gracefully) +curl http://localhost:9050/payment/catalog/filtered/products/999999/available + +# Expected Response: +{ + "productId": 999999, + "available": false, + "method": "REST Client with Custom Filters", + "note": "Product not found - ResponseLoggingFilter logged 404 before returning false" +} +---- + +**Compare Filtered vs Non-Filtered Clients:** + +[source,bash] +---- +# Side-by-side comparison endpoint +curl http://localhost:9050/payment/catalog/compare/1 + +# This endpoint calls BOTH clients: +# 1. Non-filtered client (minimal logging) +# 2. Filtered client (comprehensive logging) + +# Compare the server logs to see the difference in observability +---- + +**Expected Log Output (Filtered Client):** + +[source,log] +---- +INFO: Getting product with custom filters: 1 +INFO: Bearer token authentication added to request +INFO: Generated new Correlation ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: === Outgoing REST Client Request === +INFO: Method: GET +INFO: URI: http://localhost:5050/catalog/api/products/1 +INFO: Request Headers: +INFO: Authorization: [REDACTED] +INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: X-Request-ID: b2c3d4e5-f6a7-8901-bcde-f12345678901 +INFO: =================================== +INFO: === Incoming REST Client Response === +INFO: Request: GET http://localhost:5050/catalog/api/products/1 +INFO: Status: 200 OK +INFO: Content-Type: application/json +INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: Result: SUCCESS +INFO: ==================================== +INFO: Successfully retrieved product with filters: Laptop +---- + +**Configure Bearer Token (Optional):** + +Edit `src/main/resources/META-INF/microprofile-config.properties`: + +[source,properties] +---- +# Uncomment and set your Bearer token +catalog-service.bearer.token=your-test-token-here +---- + +Restart the server and test again - you'll see the Authorization header added. + +**Verify Filter Priority:** + +Filter execution order is determined by `@Priority` values (lower numbers execute first): + +[source] +---- +Request Flow: +1. BearerTokenFilter (Priority 1000 - AUTHENTICATION) +2. CorrelationIdFilter (Priority 100) +3. RequestLoggingFilter (Priority 300) +4. --- HTTP REQUEST SENT --- +5. --- HTTP RESPONSE RECEIVED --- +6. ResponseLoggingFilter (Priority 300) +---- + +**Files to Review:** + +* [BearerTokenFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java) - Authentication filter +* [CorrelationIdFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java) - Distributed tracing +* [RequestLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java) - Request logging +* [ResponseLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java) - Response logging +* [ProductClientWithFilters.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java) - Client with filters registered + +**Benefits of Using Filters:** + +[source,bash] +---- +# Test the comparison endpoint to see benefits +curl http://localhost:9050/payment/catalog/compare/1 + +# Response shows benefits: +{ + "comparison": { + "noFilters": "Minimal logging - only business logic logs", + "withFilters": "Comprehensive logging - authentication, tracing, request/response details" + }, + "filterBenefits": [ + "Authentication without changing business code", + "Distributed tracing with correlation IDs", + "Complete request/response visibility", + "Easier debugging and monitoring", + "Cross-cutting concerns separated from business logic" + ] +} +---- + +**Common Use Cases for Filters:** + +* **Authentication**: Inject API keys, Bearer tokens, Basic Auth credentials +* **Tracing**: Propagate correlation IDs, request IDs, span IDs across services +* **Logging**: Debug requests/responses without modifying business logic +* **Headers**: Add custom headers, propagate context from incoming requests +* **Monitoring**: Collect metrics, measure latency, track error rates +* **Security**: Sign requests, validate responses, enforce policies + +**Security Best Practices:** + +1. Never log sensitive data (passwords, tokens, PII) in production +2. Use [REDACTED] masks for sensitive headers in logs +3. Store tokens in configuration, not hardcoded +4. Use environment-specific configuration for different deployments +5. Implement proper error handling in filters + +=== Example 7: RestClientBuilder for Programmatic Client Creation + +This example demonstrates creating REST clients programmatically without CDI injection. + +**What to Verify:** + +* Creating clients with `RestClientBuilder.newBuilder()` +* Using `baseUri(String)` method (MicroProfile Rest Client 4.0) +* Try-with-resources pattern with `AutoCloseable` +* Dynamic configuration from MicroProfile Config +* Environment-specific client configuration + +**Test Basic RestClientBuilder:** + +[source,bash] +---- +# Check product availability with programmatic client +curl http://localhost:9050/payment/catalog/builder/products/1/check + +# Expected Response: +{ + "productId": 1, + "available": true, + "method": "RestClientBuilder (programmatic)", + "note": "Product found using programmatically created client" +} +---- + +**Test Dynamic Configuration:** + +[source,bash] +---- +# Get product with configuration loaded from microprofile-config.properties +curl http://localhost:9050/payment/catalog/builder/products/1/dynamic + +# Expected Response: +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "method": "RestClientBuilder with dynamic config", + "note": "Configuration loaded from microprofile-config.properties at runtime" +} +---- + +**Test Environment-Specific Configuration:** + +[source,bash] +---- +# Get product for development environment +curl http://localhost:9050/payment/catalog/builder/products/1/env/dev + +# Get product for staging environment (will fail if service not available) +curl http://localhost:9050/payment/catalog/builder/products/1/env/staging + +# Get product for production environment (will fail if service not available) +curl http://localhost:9050/payment/catalog/builder/products/1/env/prod + +# Expected Response (dev): +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "environment": "dev", + "method": "RestClientBuilder with environment-specific config", + "note": "Client configured for dev environment" +} +---- + +**Test Batch Processing:** + +[source,bash] +---- +# Check multiple products in batch +curl -X POST http://localhost:9050/payment/catalog/builder/products/batch-check \ + -H "Content-Type: application/json" \ + -d '{"productIds": [1, 2, 3, 999, 1000]}' + +# Expected Response: +{ + "totalChecked": 5, + "availableCount": 3, + "unavailableCount": 2, + "method": "RestClientBuilder (batch processing)", + "note": "Each product checked with a separate programmatically created client instance" +} +---- + +**When to Use RestClientBuilder:** + +* CDI is unavailable or not desired +* Client configuration must be determined dynamically at runtime +* Creating multiple clients with different configurations +* Utility methods or batch jobs +* Testing environments where CDI injection is complex + +**Files to Review:** + +* [ProductClientBuilderService.java](src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java) - Programmatic client creation examples +* [ProductClient.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java) - Note `extends AutoCloseable` + +=== Learning Objective + +After completing these examples, you should understand: + +- How to define a REST client interface with `@RegisterRestClient` +- How to configure REST clients using MicroProfile Config properties +- How to inject REST clients with `@Inject` and `@RestClient` in CDI beans +- How to use parameter annotations (`@PathParam`, `@QueryParam`, `@HeaderParam`) +- How to implement `ResponseExceptionMapper` for custom error handling +- The difference between checked and unchecked exceptions in exception mappers +- How to register exception mappers with `@RegisterProvider` +- How to extract error messages from JSON response bodies +- How CDI bean discovery works with `beans.xml` +- How to test REST clients in a microservices environment +- How to implement custom `ClientRequestFilter` for request interception +- How to implement custom `ClientResponseFilter` for response logging +- How to use `@Priority` to control filter execution order +- How to register filters using `@RegisterProvider` annotation +- How to separate cross-cutting concerns (auth, logging, tracing) from business logic +- How to create REST clients programmatically using `RestClientBuilder` +- How to use the `baseUri(String)` method in MicroProfile Rest Client 4.0 +- When to use `RestClientBuilder` vs CDI injection +- How to use try-with-resources with `AutoCloseable` REST clients +- How to dynamically configure REST clients at runtime + +=== Troubleshooting Common Issues + +**Issue: "Unsatisfied dependencies for type ProductClient"** + +*Solution:* Ensure `beans.xml` exists in `src/main/webapp/WEB-INF/` with `bean-discovery-mode="all"` + +**Issue: "Connection refused" errors** + +*Solution:* Verify the catalog service is running on port 5050: +[source,bash] +---- +curl http://localhost:5050/catalog/api/products +---- + +**Issue: "Read timeout" errors** + +*Solution:* Increase timeout values in `microprofile-config.properties`: +[source,properties] +---- +catalog-service/mp-rest/readTimeout=10000 +---- + +**Issue: Exceptions not being mapped correctly** + +*Solution:* Verify `@RegisterProvider` is present on the client interface and the mapper implements `ResponseExceptionMapper` + == Project Structure [source] @@ -320,13 +1109,3 @@ service/ * *Metrics*: Performance monitoring * *OpenAPI*: API documentation * *Rest Client*: Type-safe REST clients - -== Development - -=== Adding a New Service - -1. Create a new directory for your service -2. Copy the basic structure from an existing service -3. Update the `pom.xml` file with appropriate details -4. Implement your service-specific functionality -5. Configure the Liberty server in `src/main/liberty/config/` From aaadda42e510e62f4d7469e29a4917523941caaf Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 18:12:40 +0000 Subject: [PATCH 4/7] Please enter the commit message for your changes. Lines starting with '#' will be ignored, and an empty message aborts the commit. --- code/chapter11/README.adoc | 1111 ----------------- code/chapter11/catalog/README.adoc | 622 --------- code/chapter11/catalog/pom.xml | 75 -- .../store/product/ProductRestApplication.java | 9 - .../store/product/entity/Product.java | 16 - .../product/repository/ProductRepository.java | 138 -- .../product/resource/ProductResource.java | 182 --- .../store/product/service/ProductService.java | 97 -- .../src/main/liberty/config/server.xml | 23 - .../META-INF/microprofile-config.properties | 5 - .../catalog/src/main/webapp/WEB-INF/web.xml | 13 - .../catalog/src/main/webapp/index.html | 281 ----- .../client/ProductServiceClient.java | 0 .../store/inventory/entity/Inventory.java | 0 .../inventory/exception/ErrorResponse.java | 0 .../exception/InventoryConflictException.java | 0 .../exception/InventoryExceptionMapper.java | 0 .../exception/InventoryNotFoundException.java | 0 .../inventory/resource/InventoryResource.java | 0 .../inventory/service/InventoryService.java | 18 +- .../{ => chapter11}/shoppingcart/Dockerfile | 0 .../{ => chapter11}/shoppingcart/README.md | 0 .../{ => chapter11}/shoppingcart/pom.xml | 8 +- .../shoppingcart/run-docker.sh | 0 .../{ => chapter11}/shoppingcart/run.sh | 0 .../shoppingcart/ShoppingCartApplication.java | 0 .../store/shoppingcart/entity/CartItem.java | 0 .../shoppingcart/entity/ShoppingCart.java | 0 .../health/ShoppingCartHealthCheck.java | 0 .../repository/ShoppingCartRepository.java | 0 .../resource/ShoppingCartResource.java | 0 .../service/ShoppingCartService.java | 0 .../META-INF/microprofile-config.properties | 0 .../src/main/webapp/WEB-INF/web.xml | 0 .../shoppingcart/src/main/webapp/index.html | 0 .../shoppingcart/src/main/webapp/index.jsp | 0 code/chapter11/docker-compose.yml | 85 -- code/chapter11/inventory/README.adoc | 387 ------ code/chapter11/inventory/pom.xml | 163 --- .../store/inventory/InventoryApplication.java | 33 - .../dto/InventoryWithProductInfo.java | 165 --- .../tutorial/store/inventory/dto/Product.java | 69 - .../store/inventory/package-info.java | 13 - .../repository/InventoryRepository.java | 168 --- .../src/main/liberty/config/server.xml | 15 - .../META-INF/microprofile-config.properties | 5 - .../src/main/webapp/WEB-INF/beans.xml | 7 - .../inventory/src/main/webapp/WEB-INF/web.xml | 10 - .../inventory/src/main/webapp/index.html | 350 ------ .../InventoryServiceIntegrationTest.java | 154 --- .../service/InventoryServiceTest.java | 302 ----- code/chapter11/order/Dockerfile | 19 - code/chapter11/order/README.md | 147 --- code/chapter11/order/pom.xml | 114 -- code/chapter11/order/restart-server.sh | 35 - code/chapter11/order/run-docker.sh | 10 - code/chapter11/order/run.sh | 12 - .../store/order/OrderApplication.java | 34 - .../tutorial/store/order/entity/Order.java | 45 - .../store/order/entity/OrderItem.java | 38 - .../store/order/entity/OrderStatus.java | 14 - .../tutorial/store/order/package-info.java | 14 - .../order/repository/OrderItemRepository.java | 124 -- .../order/repository/OrderRepository.java | 109 -- .../order/resource/OrderItemResource.java | 149 --- .../store/order/resource/OrderResource.java | 208 --- .../store/order/service/OrderService.java | 360 ------ .../order/src/main/liberty/config/server.xml | 16 - .../order/src/main/webapp/WEB-INF/web.xml | 10 - .../order/src/main/webapp/index.html | 144 --- .../src/main/webapp/order-status-codes.html | 75 -- code/chapter11/payment/Dockerfile | 20 - code/chapter11/payment/README.adoc | 266 ---- code/chapter11/payment/pom.xml | 85 -- code/chapter11/payment/run-docker.sh | 23 - code/chapter11/payment/run.sh | 19 - .../tutorial/PaymentRestApplication.java | 9 - .../store/payment/client/ProductClient.java | 52 - .../payment/client/ProductClientJson.java | 55 - .../store/payment/config/PaymentConfig.java | 63 - .../config/PaymentServiceConfigSource.java | 60 - .../store/payment/dto/product/Product.java | 10 - .../store/payment/entity/PaymentDetails.java | 18 - .../examples/ProductClientExample.java | 71 -- .../resource/PaymentConfigResource.java | 98 -- .../resource/PaymentProductResource.java | 186 --- .../store/payment/service/PaymentService.java | 46 - .../service/ProductIntegrationService.java | 148 --- .../store/payment/service/payment.http | 9 - .../src/main/liberty/config/server.xml | 20 - .../META-INF/microprofile-config.properties | 11 - ...lipse.microprofile.config.spi.ConfigSource | 1 - .../payment/src/main/webapp/WEB-INF/web.xml | 12 - .../payment/src/main/webapp/index.html | 140 --- .../payment/src/main/webapp/index.jsp | 12 - code/chapter11/run-all-services.sh | 36 - code/chapter11/shipment/Dockerfile | 27 - code/chapter11/shipment/README.md | 87 -- code/chapter11/shipment/pom.xml | 114 -- code/chapter11/shipment/run-docker.sh | 11 - code/chapter11/shipment/run.sh | 12 - .../store/shipment/ShipmentApplication.java | 35 - .../store/shipment/client/OrderClient.java | 193 --- .../store/shipment/entity/Shipment.java | 45 - .../store/shipment/entity/ShipmentStatus.java | 16 - .../store/shipment/filter/CorsFilter.java | 43 - .../shipment/health/ShipmentHealthCheck.java | 67 - .../repository/ShipmentRepository.java | 148 --- .../shipment/resource/ShipmentResource.java | 397 ------ .../shipment/service/ShipmentService.java | 305 ----- .../src/main/liberty/config/server.xml | 18 - .../META-INF/microprofile-config.properties | 32 - .../shipment/src/main/webapp/WEB-INF/web.xml | 13 - .../shipment/src/main/webapp/index.html | 150 --- .../shoppingcart/client/CatalogClient.java | 184 --- .../shoppingcart/client/InventoryClient.java | 96 -- .../src/main/liberty/config/server.xml | 16 - code/chapter11/user/README.adoc | 280 ----- code/chapter11/user/pom.xml | 115 -- .../tutorial/store/user/UserApplication.java | 12 - .../tutorial/store/user/entity/User.java | 75 -- .../store/user/entity/package-info.java | 6 - .../tutorial/store/user/package-info.java | 6 - .../store/user/repository/UserRepository.java | 135 -- .../store/user/repository/package-info.java | 6 - .../store/user/resource/UserResource.java | 132 -- .../store/user/resource/package-info.java | 0 .../store/user/service/UserService.java | 130 -- .../store/user/service/package-info.java | 0 .../user/src/main/liberty/config/server.xml | 16 - .../chapter11/user/src/main/webapp/index.html | 107 -- 131 files changed, 12 insertions(+), 10688 deletions(-) delete mode 100644 code/chapter11/README.adoc delete mode 100644 code/chapter11/catalog/README.adoc delete mode 100644 code/chapter11/catalog/pom.xml delete mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java delete mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java delete mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java delete mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java delete mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java delete mode 100644 code/chapter11/catalog/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/catalog/src/main/webapp/index.html rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java (100%) rename code/chapter11/{ => chapter11}/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java (97%) rename code/chapter11/{ => chapter11}/shoppingcart/Dockerfile (100%) rename code/chapter11/{ => chapter11}/shoppingcart/README.md (100%) rename code/chapter11/{ => chapter11}/shoppingcart/pom.xml (95%) rename code/chapter11/{ => chapter11}/shoppingcart/run-docker.sh (100%) mode change 100755 => 100644 rename code/chapter11/{ => chapter11}/shoppingcart/run.sh (100%) mode change 100755 => 100644 rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/resources/META-INF/microprofile-config.properties (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/webapp/WEB-INF/web.xml (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/webapp/index.html (100%) rename code/chapter11/{ => chapter11}/shoppingcart/src/main/webapp/index.jsp (100%) delete mode 100644 code/chapter11/docker-compose.yml delete mode 100644 code/chapter11/inventory/README.adoc delete mode 100644 code/chapter11/inventory/pom.xml delete mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java delete mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java delete mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java delete mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java delete mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java delete mode 100644 code/chapter11/inventory/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml delete mode 100644 code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/inventory/src/main/webapp/index.html delete mode 100644 code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java delete mode 100644 code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java delete mode 100644 code/chapter11/order/Dockerfile delete mode 100644 code/chapter11/order/README.md delete mode 100644 code/chapter11/order/pom.xml delete mode 100755 code/chapter11/order/restart-server.sh delete mode 100755 code/chapter11/order/run-docker.sh delete mode 100755 code/chapter11/order/run.sh delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java delete mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java delete mode 100644 code/chapter11/order/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/order/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/order/src/main/webapp/index.html delete mode 100644 code/chapter11/order/src/main/webapp/order-status-codes.html delete mode 100644 code/chapter11/payment/Dockerfile delete mode 100644 code/chapter11/payment/README.adoc delete mode 100644 code/chapter11/payment/pom.xml delete mode 100755 code/chapter11/payment/run-docker.sh delete mode 100755 code/chapter11/payment/run.sh delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientJson.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/examples/ProductClientExample.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java delete mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http delete mode 100644 code/chapter11/payment/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource delete mode 100644 code/chapter11/payment/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/payment/src/main/webapp/index.html delete mode 100644 code/chapter11/payment/src/main/webapp/index.jsp delete mode 100755 code/chapter11/run-all-services.sh delete mode 100644 code/chapter11/shipment/Dockerfile delete mode 100644 code/chapter11/shipment/README.md delete mode 100644 code/chapter11/shipment/pom.xml delete mode 100755 code/chapter11/shipment/run-docker.sh delete mode 100755 code/chapter11/shipment/run.sh delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java delete mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java delete mode 100644 code/chapter11/shipment/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/shipment/src/main/webapp/index.html delete mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java delete mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java delete mode 100644 code/chapter11/shoppingcart/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/user/README.adoc delete mode 100644 code/chapter11/user/pom.xml delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java delete mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java delete mode 100644 code/chapter11/user/src/main/liberty/config/server.xml delete mode 100644 code/chapter11/user/src/main/webapp/index.html diff --git a/code/chapter11/README.adoc b/code/chapter11/README.adoc deleted file mode 100644 index 593f1c90..00000000 --- a/code/chapter11/README.adoc +++ /dev/null @@ -1,1111 +0,0 @@ -= MicroProfile E-Commerce Store -:toc: left -:icons: font -:source-highlighter: highlightjs -:imagesdir: images -:experimental: - -== Overview - -This project demonstrates a microservices-based e-commerce application built with Jakarta EE and MicroProfile, running on Open Liberty runtime. -The application is composed of multiple independent services that work together to provide a complete e-commerce solution. - -== Services - -The application is split into the following microservices: - -[cols="1,4", options="header"] -|=== -|Service |Description - -|User Service -|Manages user accounts, authentication, and profile information - -|Inventory Service -|Tracks product inventory and stock levels - -|Order Service -|Manages customer orders, order items, and order status - -|Catalog Service -|Provides product information, categories, and search capabilities - -|Shopping Cart Service -|Manages user shopping cart items and temporary product storage - -|Shipment Service -|Handles shipping orders, tracking, and delivery status updates - -|Payment Service -|Processes payments and manages payment methods and transactions. Demonstrates MicroProfile Rest Client 4.0 features including CDI injection, ResponseExceptionMapper, custom filters, and programmatic client creation. -|=== - -== Technology Stack - -* *Jakarta EE 10.0*: For enterprise Java standardization -* *MicroProfile 7.x*: For cloud-native APIs -* *MicroProfile Rest Client 4.0*: Type-safe REST service consumption -* *Open Liberty*: Lightweight, flexible runtime for Java microservices -* *Maven*: For project management and builds - -== Quick Start - -=== Prerequisites - -* JDK 21 or later -* Maven 3.9 or later -* Docker (optional for containerized deployment) - -=== Running the Application - -1. Clone the repository: -+ -[source,bash] ----- -git clone https://github.com/microprofile/microprofile-tutorial.git -cd code/chapter11 ----- - -2. Start each microservice individually: - -==== User Service -[source,bash] ----- -cd user -mvn liberty:run ----- -The service will be available at http://localhost:6050/user - -==== Inventory Service -[source,bash] ----- -cd inventory -mvn liberty:run ----- -The service will be available at http://localhost:7050/inventory - -==== Order Service -[source,bash] ----- -cd order -mvn liberty:run ----- -The service will be available at http://localhost:8050/order - -==== Catalog Service -[source,bash] ----- -cd catalog -mvn liberty:run ----- -The service will be available at http://localhost:9050/catalog - -=== Building the Application - -To build all services: - -[source,bash] ----- -mvn clean package ----- - -=== Docker Deployment - -You can also run all services together using Docker Compose: - -[source,bash] ----- -# Make the script executable (if needed) -chmod +x run-all-services.sh - -# Run the script to build and start all services -./run-all-services.sh ----- - -Or manually: - -[source,bash] ----- -# Build all projects first -cd user && mvn clean package && cd .. -cd inventory && mvn clean package && cd .. -cd order && mvn clean package && cd .. -cd catalog && mvn clean package && cd .. - -# Start all services -docker-compose up -d ----- - -This will start all services in Docker containers with the following endpoints: - -* User Service: http://localhost:6050/user -* Inventory Service: http://localhost:7050/inventory -* Order Service: http://localhost:8050/order -* Catalog Service: http://localhost:9050/catalog - -== API Documentation - -Each microservice provides its own OpenAPI documentation, available at: - -* User Service: http://localhost:6050/user/openapi -* Inventory Service: http://localhost:7050/inventory/openapi -* Order Service: http://localhost:8050/order/openapi -* Catalog Service: http://localhost:9050/catalog/openapi - -== MicroProfile Rest Client 4.0 Features - -This application demonstrates the latest MicroProfile Rest Client 4.0 features: - -=== New `baseUri(String)` Convenience Method - -The Inventory Service showcases the new `baseUri(String)` method that eliminates the need for `URI.create()`: - -[source,java] ----- -// MicroProfile Rest Client 4.0 - Simplified approach -ProductServiceClient client = RestClientBuilder.newBuilder() - .baseUri("http://localhost:5050/catalog/api") // Direct String parameter - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(ProductServiceClient.class); ----- - -This replaces the older pattern: - -[source,java] ----- -// Old approach (Rest Client 3.x) -URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); -ProductServiceClient client = RestClientBuilder.newBuilder() - .baseUri(catalogServiceUri) // Required URI object - .build(ProductServiceClient.class); ----- - -=== CDI Integration with `@RestClient` - -The `@RestClient` qualifier is mandatory for CDI injection: - -[source,java] ----- -@Inject -@RestClient // Required qualifier -private ProductServiceClient productClient; ----- - -=== AutoCloseable Support - -REST client interfaces extend `AutoCloseable` for proper resource management: - -[source,java] ----- -@RegisterRestClient(configKey = "product-service") -@Path("/products") -public interface ProductServiceClient extends AutoCloseable { - // Client methods -} ----- - -See the `InventoryService` class for complete working examples of these features. - -== Testing the Services - -=== User Service - -[source,bash] ----- -# Get all users -curl -X GET http://localhost:6050/user/api/users - -# Create a new user -curl -X POST http://localhost:6050/user/api/users \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Jane Doe", - "email": "jane@example.com", - "passwordHash": "password123", - "address": "123 Main St", - "phoneNumber": "555-123-4567" - }' - -# Get a user by ID -curl -X GET http://localhost:6050/user/api/users/1 - -# Update a user -curl -X PUT http://localhost:6050/user/api/users/1 \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Jane Smith", - "email": "jane@example.com", - "passwordHash": "password123", - "address": "456 Oak Ave", - "phoneNumber": "555-123-4567" - }' - -# Delete a user -curl -X DELETE http://localhost:6050/user/api/users/1 ----- - -=== Inventory Service - -[source,bash] ----- -# Get all inventory items -curl -X GET http://localhost:7050/inventory/api/inventories - -# Create a new inventory item -curl -X POST http://localhost:7050/inventory/api/inventories \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 101, - "quantity": 25 - }' - -# Get inventory by ID -curl -X GET http://localhost:7050/inventory/api/inventories/1 - -# Get inventory by product ID -curl -X GET http://localhost:7050/inventory/api/inventories/product/101 - -# Update inventory -curl -X PUT http://localhost:7050/inventory/api/inventories/1 \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 101, - "quantity": 50 - }' - -# Update product quantity -curl -X PATCH http://localhost:7050/inventory/api/inventories/product/101/quantity/75 - -# Delete inventory -curl -X DELETE http://localhost:7050/inventory/api/inventories/1 ----- - -=== Order Service - -[source,bash] ----- -# Get all orders -curl -X GET http://localhost:8050/order/api/orders - -# Create a new order -curl -X POST http://localhost:8050/order/api/orders \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "CREATED", - "orderItems": [ - { - "productId": 101, - "quantity": 2, - "priceAtOrder": 49.99 - }, - { - "productId": 102, - "quantity": 1, - "priceAtOrder": 50.00 - } - ] - }' - -# Get order by ID -curl -X GET http://localhost:8050/order/api/orders/1 - -# Update order status -curl -X PATCH http://localhost:8050/order/api/orders/1/status/PAID - -# Get items for an order -curl -X GET http://localhost:8050/order/api/orders/1/items - -# Delete order -curl -X DELETE http://localhost:8050/order/api/orders/1 ----- - -=== Catalog Service - -[source,bash] ----- -# Get all products -curl -X GET http://localhost:9050/catalog/api/products - -# Get a product by ID -curl -X GET http://localhost:9050/catalog/api/products/1 - -# Search products -curl -X GET "http://localhost:9050/catalog/api/products/search?keyword=laptop" ----- - -== Verifying MicroProfile Rest Client Examples - -The Payment Service demonstrates comprehensive MicroProfile Rest Client 4.0 features. Follow these steps to verify each example. - -=== Prerequisites - -Before testing the examples, ensure both services are running: - -[source,bash] ----- -# Terminal 1 - Start the Catalog Service (provides product data) -cd catalog -mvn liberty:run - -# Terminal 2 - Start the Payment Service (consumes catalog service) -cd payment -mvn liberty:run ----- - -The services will be available at: - -* Catalog Service: http://localhost:5050/catalog -* Payment Service: http://localhost:9050/payment - -=== Example 1: Basic REST Client with @RegisterRestClient - -This example demonstrates defining a MicroProfile Rest Client interface with `@RegisterRestClient`. - -**What to Verify:** - -* `ProductClient` interface is annotated with `@RegisterRestClient(configKey = "catalog-service")` -* The client uses Jakarta REST annotations (`@GET`, `@Path`, `@PathParam`) -* Configuration is externalized in `microprofile-config.properties` - -**Test the Client:** - -[source,bash] ----- -# Get all products via REST client -curl http://localhost:9050/payment/catalog/products - -# Expected Response: JSON array of products -[ - { - "id": 1, - "name": "Laptop", - "price": 999.99 - }, - ... -] ----- - -**Files to Review:** - -* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` -* `src/main/resources/META-INF/microprofile-config.properties` - -=== Example 2: MicroProfile Config Integration - -This example shows externalized configuration for REST clients using MicroProfile Config. - -**What to Verify:** - -* Configuration properties follow the pattern: `/mp-rest/` -* Base URL, timeouts, and scope are configured -* Properties can be overridden via environment variables - -**Configuration Properties:** - -[source,properties] ----- -# Base URL -catalog-service/mp-rest/url=http://localhost:5050/catalog/api - -# Timeouts (in milliseconds) -catalog-service/mp-rest/connectTimeout=3000 -catalog-service/mp-rest/readTimeout=5000 - -# CDI Scope -catalog-service/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped - -# Follow redirects -catalog-service/mp-rest/followRedirects=true ----- - -**Test Configuration:** - -[source,bash] ----- -# Verify the client respects timeout settings -# (This will timeout if catalog service is slow) -time curl http://localhost:9050/payment/catalog/products ----- - -**Files to Review:** - -* `src/main/resources/META-INF/microprofile-config.properties` - -=== Example 3: CDI Injection with @Inject and @RestClient - -This example demonstrates dependency injection of REST clients using CDI. - -**What to Verify:** - -* `ProductClient` is injected with `@Inject` and `@RestClient` qualifiers -* The `@RestClient` qualifier is mandatory in MicroProfile Rest Client 4.0 -* Service layer (`ProductCatalogService`) uses the injected client -* `@ApplicationScoped` bean lifecycle management - -**Test CDI Injection:** - -[source,bash] ----- -# Test service that uses injected REST client -curl http://localhost:9050/payment/catalog/products - -# Get specific product (uses getProductById method) -curl http://localhost:9050/payment/catalog/products/1 - -# Expected Response: -{ - "id": 1, - "name": "Laptop", - "price": 999.99 -} ----- - -**Files to Review:** - -* `src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java` -* `src/main/webapp/WEB-INF/beans.xml` (CDI configuration) - -=== Example 4: Parameter Annotations (@PathParam, @QueryParam) - -This example shows different parameter types in REST client methods. - -**What to Verify:** - -* `@PathParam` for path variables (e.g., `/products/{id}`) -* `@QueryParam` for query string parameters (e.g., `?price=99.99`) -* Parameters are automatically encoded and passed to remote service - -**Test Path Parameters:** - -[source,bash] ----- -# Test @PathParam - Get product by ID -curl http://localhost:9050/payment/catalog/products/1 - -# Test with different IDs -curl http://localhost:9050/payment/catalog/products/2 -curl http://localhost:9050/payment/catalog/products/3 ----- - -**Test Query Parameters:** - -[source,bash] ----- -# Test @QueryParam - Validate product price -curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" - -# Expected Response (if price matches): -{ - "productId": 1, - "expectedPrice": 999.99, - "valid": true, - "message": "Price matches" -} - -# Test with wrong price -curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=500.00" - -# Expected Response (if price doesn't match): -{ - "productId": 1, - "expectedPrice": 500.0, - "valid": false, - "message": "Price mismatch detected" -} ----- - -**Files to Review:** - -* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (method signatures) -* `src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java` (endpoint implementation) - -=== Example 5: ResponseExceptionMapper - Custom Error Handling - -This example demonstrates mapping HTTP error responses to custom exceptions. - -**What to Verify:** - -* `ProductServiceResponseExceptionMapper` implements `ResponseExceptionMapper` -* HTTP 404 responses map to `ProductNotFoundException` (checked exception) -* HTTP 503 responses map to `ServiceUnavailableException` (unchecked exception) -* Error messages are extracted from JSON response bodies -* `@Priority` annotation controls mapper ordering - -**Test Exception Mapping:** - -[source,bash] ----- -# Test successful request (no exception) -curl http://localhost:9050/payment/catalog/products/1/detailed - -# Expected Response: -{ - "productId": 1, - "product": { - "id": 1, - "name": "Laptop", - "price": 999.99 - }, - "note": "Product retrieved successfully - no exceptions thrown" -} - -# Test 404 error (ProductNotFoundException) -curl http://localhost:9050/payment/catalog/products/999999/detailed - -# Expected Response: -{ - "productId": 999999, - "error": "Product not found", - "note": "ProductNotFoundException was caught and handled by the service layer" -} - -# Test product availability (handles 404 gracefully) -curl http://localhost:9050/payment/catalog/products/999999/availability - -# Expected Response: -{ - "productId": 999999, - "available": false, - "message": "Product is not available" -} ----- - -**Verify Exception Types:** - -1. **Checked Exception (ProductNotFoundException)**: - - Must be declared in method signature: `throws ProductNotFoundException` - - Only thrown if method declares it - - Handled gracefully in service layer - -2. **Unchecked Exception (ServiceUnavailableException)**: - - Extends `RuntimeException` - - Always thrown regardless of method signature - - Used for service availability errors - -**Test Service Unavailable Scenario:** - -[source,bash] ----- -# Stop the catalog service to simulate 503 error -# Then try to access products -curl http://localhost:9050/payment/catalog/products/1/detailed - -# Expected Response (if catalog service is down): -{ - "productId": 1, - "error": "Catalog service unavailable", - "statusCode": 503, - "message": "...", - "note": "ServiceUnavailableException (unchecked) was thrown by ResponseExceptionMapper" -} ----- - -**Files to Review:** - -* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java` -* `src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java` -* `src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java` -* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (see `@RegisterProvider`) - -=== Verifying beans.xml Configuration - -The `beans.xml` file is required for CDI bean discovery and REST client registration. - -**What to Verify:** - -* File location: `src/main/webapp/WEB-INF/beans.xml` -* Bean discovery mode: `bean-discovery-mode="all"` -* Version: Jakarta EE 10 (beans 4.0) - -[source,xml] ----- - - - ----- - -**Files to Review:** - -* `src/main/webapp/WEB-INF/beans.xml` - -=== Additional Testing Scenarios - -==== Test Complete Product Validation Flow - -[source,bash] ----- -# 1. Check if product is available -curl http://localhost:9050/payment/catalog/products/1/availability - -# 2. Validate product price before payment -curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" - -# 3. Get product details for payment processing -curl http://localhost:9050/payment/catalog/products/1 ----- - -==== Test Error Scenarios - -[source,bash] ----- -# Invalid product ID (404) -curl http://localhost:9050/payment/catalog/products/999999 - -# Missing query parameter (400) -curl http://localhost:9050/payment/catalog/products/1/validate-price - -# Invalid price value (400) -curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=-10" ----- - -=== Viewing OpenAPI Documentation - -Access the OpenAPI documentation to see all available endpoints: - -[source,bash] ----- -# Payment Service OpenAPI documentation -open http://localhost:9050/openapi/ui/ - -# Or view the OpenAPI JSON spec -curl http://localhost:9050/openapi ----- - -=== Debugging Tips - -**Enable Detailed Logging:** - -Edit `src/main/liberty/config/server.xml` and add: - -[source,xml] ----- - ----- - -**Check Server Logs:** - -[source,bash] ----- -# View real-time logs -tail -f target/liberty/wlp/usr/servers/mpServer/logs/messages.log - -# Search for REST client activity -grep "ProductClient" target/liberty/wlp/usr/servers/mpServer/logs/messages.log ----- - -**Verify Configuration Loading:** - -[source,bash] ----- -# Check if properties are loaded correctly -# Look for log messages about MicroProfile Config -grep "mp-rest" target/liberty/wlp/usr/servers/mpServer/logs/messages.log ----- - -=== Example 6: Custom Filters and Interceptors - -This example demonstrates implementing custom filters for cross-cutting concerns like authentication, logging, and distributed tracing. - -**What to Verify:** - -* `ClientRequestFilter` implementations for request interception -* `ClientResponseFilter` implementations for response logging -* Filter registration using `@RegisterProvider` -* Filter execution order using `@Priority` -* Cross-cutting concerns separated from business logic - -**Filters Implemented:** - -1. **BearerTokenFilter** (Priority 1000 - AUTHENTICATION) - - Adds Authorization header with Bearer token - - Reads token from MicroProfile Config - - Runs first due to authentication priority - -2. **CorrelationIdFilter** (Priority 100) - - Adds X-Correlation-ID for distributed tracing - - Generates or propagates correlation IDs - - Adds unique X-Request-ID for each request - -3. **RequestLoggingFilter** (Priority 300) - - Logs complete outgoing request details - - Masks sensitive headers (Authorization, API keys) - - Runs after authentication and correlation filters - -4. **ResponseLoggingFilter** (Priority 300) - - Logs complete incoming response details - - Includes status codes, headers, timing - - Correlates responses with requests - -**Test Basic Filter Functionality:** - -[source,bash] ----- -# Get all products with filters (watch server logs) -curl http://localhost:9050/payment/catalog/filtered/products - -# Expected in logs: -# 1. "Bearer token authentication added to request" (if token configured) -# 2. "Generated new Correlation ID: " -# 3. "=== Outgoing REST Client Request ===" -# 4. "Method: GET" -# 5. "URI: http://localhost:5050/catalog/api/products" -# 6. "=== Incoming REST Client Response ===" -# 7. "Status: 200 OK" ----- - -**Test Filter Execution with Specific Product:** - -[source,bash] ----- -# Get specific product - observe filter chain execution -curl http://localhost:9050/payment/catalog/filtered/products/1 - -# Expected Response: -{ - "product": { - "id": 1, - "name": "Laptop", - "price": 999.99 - }, - "method": "REST Client with Custom Filters", - "filterChain": "BearerToken → CorrelationId → RequestLogging → [HTTP] → ResponseLogging", - "note": "Check server logs for X-Correlation-ID and detailed request/response traces" -} ----- - -**Test Filters with Error Responses:** - -[source,bash] ----- -# Test 404 error - filters still execute and log the error -curl http://localhost:9050/payment/catalog/filtered/products/999999 - -# Expected in logs: -# - RequestLoggingFilter logs the outgoing request -# - ResponseLoggingFilter logs: "Status: 404 Not Found" -# - ResponseLoggingFilter logs: "Result: CLIENT ERROR (404)" -# - ProductNotFoundException is thrown after logging - -# Check product availability (handles 404 gracefully) -curl http://localhost:9050/payment/catalog/filtered/products/999999/available - -# Expected Response: -{ - "productId": 999999, - "available": false, - "method": "REST Client with Custom Filters", - "note": "Product not found - ResponseLoggingFilter logged 404 before returning false" -} ----- - -**Compare Filtered vs Non-Filtered Clients:** - -[source,bash] ----- -# Side-by-side comparison endpoint -curl http://localhost:9050/payment/catalog/compare/1 - -# This endpoint calls BOTH clients: -# 1. Non-filtered client (minimal logging) -# 2. Filtered client (comprehensive logging) - -# Compare the server logs to see the difference in observability ----- - -**Expected Log Output (Filtered Client):** - -[source,log] ----- -INFO: Getting product with custom filters: 1 -INFO: Bearer token authentication added to request -INFO: Generated new Correlation ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 -INFO: === Outgoing REST Client Request === -INFO: Method: GET -INFO: URI: http://localhost:5050/catalog/api/products/1 -INFO: Request Headers: -INFO: Authorization: [REDACTED] -INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 -INFO: X-Request-ID: b2c3d4e5-f6a7-8901-bcde-f12345678901 -INFO: =================================== -INFO: === Incoming REST Client Response === -INFO: Request: GET http://localhost:5050/catalog/api/products/1 -INFO: Status: 200 OK -INFO: Content-Type: application/json -INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 -INFO: Result: SUCCESS -INFO: ==================================== -INFO: Successfully retrieved product with filters: Laptop ----- - -**Configure Bearer Token (Optional):** - -Edit `src/main/resources/META-INF/microprofile-config.properties`: - -[source,properties] ----- -# Uncomment and set your Bearer token -catalog-service.bearer.token=your-test-token-here ----- - -Restart the server and test again - you'll see the Authorization header added. - -**Verify Filter Priority:** - -Filter execution order is determined by `@Priority` values (lower numbers execute first): - -[source] ----- -Request Flow: -1. BearerTokenFilter (Priority 1000 - AUTHENTICATION) -2. CorrelationIdFilter (Priority 100) -3. RequestLoggingFilter (Priority 300) -4. --- HTTP REQUEST SENT --- -5. --- HTTP RESPONSE RECEIVED --- -6. ResponseLoggingFilter (Priority 300) ----- - -**Files to Review:** - -* [BearerTokenFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java) - Authentication filter -* [CorrelationIdFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java) - Distributed tracing -* [RequestLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java) - Request logging -* [ResponseLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java) - Response logging -* [ProductClientWithFilters.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java) - Client with filters registered - -**Benefits of Using Filters:** - -[source,bash] ----- -# Test the comparison endpoint to see benefits -curl http://localhost:9050/payment/catalog/compare/1 - -# Response shows benefits: -{ - "comparison": { - "noFilters": "Minimal logging - only business logic logs", - "withFilters": "Comprehensive logging - authentication, tracing, request/response details" - }, - "filterBenefits": [ - "Authentication without changing business code", - "Distributed tracing with correlation IDs", - "Complete request/response visibility", - "Easier debugging and monitoring", - "Cross-cutting concerns separated from business logic" - ] -} ----- - -**Common Use Cases for Filters:** - -* **Authentication**: Inject API keys, Bearer tokens, Basic Auth credentials -* **Tracing**: Propagate correlation IDs, request IDs, span IDs across services -* **Logging**: Debug requests/responses without modifying business logic -* **Headers**: Add custom headers, propagate context from incoming requests -* **Monitoring**: Collect metrics, measure latency, track error rates -* **Security**: Sign requests, validate responses, enforce policies - -**Security Best Practices:** - -1. Never log sensitive data (passwords, tokens, PII) in production -2. Use [REDACTED] masks for sensitive headers in logs -3. Store tokens in configuration, not hardcoded -4. Use environment-specific configuration for different deployments -5. Implement proper error handling in filters - -=== Example 7: RestClientBuilder for Programmatic Client Creation - -This example demonstrates creating REST clients programmatically without CDI injection. - -**What to Verify:** - -* Creating clients with `RestClientBuilder.newBuilder()` -* Using `baseUri(String)` method (MicroProfile Rest Client 4.0) -* Try-with-resources pattern with `AutoCloseable` -* Dynamic configuration from MicroProfile Config -* Environment-specific client configuration - -**Test Basic RestClientBuilder:** - -[source,bash] ----- -# Check product availability with programmatic client -curl http://localhost:9050/payment/catalog/builder/products/1/check - -# Expected Response: -{ - "productId": 1, - "available": true, - "method": "RestClientBuilder (programmatic)", - "note": "Product found using programmatically created client" -} ----- - -**Test Dynamic Configuration:** - -[source,bash] ----- -# Get product with configuration loaded from microprofile-config.properties -curl http://localhost:9050/payment/catalog/builder/products/1/dynamic - -# Expected Response: -{ - "product": { - "id": 1, - "name": "Laptop", - "price": 999.99 - }, - "method": "RestClientBuilder with dynamic config", - "note": "Configuration loaded from microprofile-config.properties at runtime" -} ----- - -**Test Environment-Specific Configuration:** - -[source,bash] ----- -# Get product for development environment -curl http://localhost:9050/payment/catalog/builder/products/1/env/dev - -# Get product for staging environment (will fail if service not available) -curl http://localhost:9050/payment/catalog/builder/products/1/env/staging - -# Get product for production environment (will fail if service not available) -curl http://localhost:9050/payment/catalog/builder/products/1/env/prod - -# Expected Response (dev): -{ - "product": { - "id": 1, - "name": "Laptop", - "price": 999.99 - }, - "environment": "dev", - "method": "RestClientBuilder with environment-specific config", - "note": "Client configured for dev environment" -} ----- - -**Test Batch Processing:** - -[source,bash] ----- -# Check multiple products in batch -curl -X POST http://localhost:9050/payment/catalog/builder/products/batch-check \ - -H "Content-Type: application/json" \ - -d '{"productIds": [1, 2, 3, 999, 1000]}' - -# Expected Response: -{ - "totalChecked": 5, - "availableCount": 3, - "unavailableCount": 2, - "method": "RestClientBuilder (batch processing)", - "note": "Each product checked with a separate programmatically created client instance" -} ----- - -**When to Use RestClientBuilder:** - -* CDI is unavailable or not desired -* Client configuration must be determined dynamically at runtime -* Creating multiple clients with different configurations -* Utility methods or batch jobs -* Testing environments where CDI injection is complex - -**Files to Review:** - -* [ProductClientBuilderService.java](src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java) - Programmatic client creation examples -* [ProductClient.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java) - Note `extends AutoCloseable` - -=== Learning Objective - -After completing these examples, you should understand: - -- How to define a REST client interface with `@RegisterRestClient` -- How to configure REST clients using MicroProfile Config properties -- How to inject REST clients with `@Inject` and `@RestClient` in CDI beans -- How to use parameter annotations (`@PathParam`, `@QueryParam`, `@HeaderParam`) -- How to implement `ResponseExceptionMapper` for custom error handling -- The difference between checked and unchecked exceptions in exception mappers -- How to register exception mappers with `@RegisterProvider` -- How to extract error messages from JSON response bodies -- How CDI bean discovery works with `beans.xml` -- How to test REST clients in a microservices environment -- How to implement custom `ClientRequestFilter` for request interception -- How to implement custom `ClientResponseFilter` for response logging -- How to use `@Priority` to control filter execution order -- How to register filters using `@RegisterProvider` annotation -- How to separate cross-cutting concerns (auth, logging, tracing) from business logic -- How to create REST clients programmatically using `RestClientBuilder` -- How to use the `baseUri(String)` method in MicroProfile Rest Client 4.0 -- When to use `RestClientBuilder` vs CDI injection -- How to use try-with-resources with `AutoCloseable` REST clients -- How to dynamically configure REST clients at runtime - -=== Troubleshooting Common Issues - -**Issue: "Unsatisfied dependencies for type ProductClient"** - -*Solution:* Ensure `beans.xml` exists in `src/main/webapp/WEB-INF/` with `bean-discovery-mode="all"` - -**Issue: "Connection refused" errors** - -*Solution:* Verify the catalog service is running on port 5050: -[source,bash] ----- -curl http://localhost:5050/catalog/api/products ----- - -**Issue: "Read timeout" errors** - -*Solution:* Increase timeout values in `microprofile-config.properties`: -[source,properties] ----- -catalog-service/mp-rest/readTimeout=10000 ----- - -**Issue: Exceptions not being mapped correctly** - -*Solution:* Verify `@RegisterProvider` is present on the client interface and the mapper implements `ResponseExceptionMapper` - -== Project Structure - -[source] ----- -liberty-rest-app/ -├── user/ # User management service -├── inventory/ # Inventory management service -├── order/ # Order management service -└── catalog/ # Product catalog service ----- - -Each service follows a similar internal structure: - -[source] ----- -service/ -├── src/ -│ ├── main/ -│ │ ├── java/ # Java source code -│ │ ├── liberty/ # Liberty server configuration -│ │ └── webapp/ # Web resources -│ └── test/ # Test code -└── pom.xml # Maven configuration ----- - -== Key MicroProfile Features Demonstrated - -* *Config*: Externalized configuration -* *Fault Tolerance*: Circuit breakers, retries, fallbacks -* *Health Checks*: Application health monitoring -* *Metrics*: Performance monitoring -* *OpenAPI*: API documentation -* *Rest Client*: Type-safe REST clients diff --git a/code/chapter11/catalog/README.adoc b/code/chapter11/catalog/README.adoc deleted file mode 100644 index cffee0be..00000000 --- a/code/chapter11/catalog/README.adoc +++ /dev/null @@ -1,622 +0,0 @@ -= MicroProfile Catalog Service -:toc: macro -:toclevels: 3 -:icons: font -:source-highlighter: highlight.js -:experimental: - -toc::[] - -== Overview - -The MicroProfile Catalog Service is a modern Jakarta EE 10 application built with MicroProfile 6.1 specifications and running on Open Liberty. This service provides a RESTful API for product catalog management with enhanced MicroProfile features. - -This project demonstrates the key capabilities of MicroProfile OpenAPI and in-memory persistence architecture. - -== Features - -* *RESTful API* using Jakarta RESTful Web Services -* *OpenAPI Documentation* with Swagger UI -* *In-Memory Persistence* using ConcurrentHashMap for thread-safe data storage -* *HTML Landing Page* with API documentation and service status -* *Maintenance Mode* support with configuration-based toggles - -== MicroProfile Features Implemented - -=== MicroProfile OpenAPI - -The application provides OpenAPI documentation for its REST endpoints. API documentation is generated automatically from annotations in the code: - -[source,java] ----- -@GET -@Produces(MediaType.APPLICATION_JSON) -@Operation(summary = "Get all products", description = "Returns a list of all products") -@APIResponses({ - @APIResponse(responseCode = "200", description = "List of products", - content = @Content(mediaType = "application/json", - schema = @Schema(implementation = Product.class))) -}) -public Response getAllProducts() { - // Implementation -} ----- - -The OpenAPI documentation is available at: `/openapi` (in various formats) and `/openapi/ui` (Swagger UI) - -=== In-Memory Persistence Architecture - -The application implements a thread-safe in-memory persistence layer using `ConcurrentHashMap`: - -[source,java] ----- -@ApplicationScoped -public class ProductRepository { - // In-memory storage using ConcurrentHashMap for thread safety - private final Map productsMap = new ConcurrentHashMap<>(); - - // ID generator - private final AtomicLong idGenerator = new AtomicLong(1); - - // CRUD operations... -} ----- - -==== Atomic ID Generation with AtomicLong - -The repository uses `java.util.concurrent.atomic.AtomicLong` for thread-safe ID generation: - -[source,java] ----- -// ID generation in createProduct method -if (product.getId() == null) { - product.setId(idGenerator.getAndIncrement()); -} ----- - -`AtomicLong` provides several key benefits: - -* *Thread Safety*: Guarantees atomic operations without explicit locking -* *Performance*: Uses efficient compare-and-swap (CAS) operations instead of locks -* *Consistency*: Ensures unique, sequential IDs even under concurrent access -* *No Synchronization*: Avoids the overhead of synchronized blocks - -===== Advanced AtomicLong Operations - -The ProductRepository implements an advanced pattern for handling both system-generated and client-provided IDs: - -[source,java] ----- -public Product createProduct(Product product) { - // Generate ID if not provided - if (product.getId() == null) { - product.setId(idGenerator.getAndIncrement()); - } else { - // Update idGenerator if the provided ID is greater than current - long nextId = product.getId() + 1; - while (true) { - long currentId = idGenerator.get(); - if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { - break; - } - } - } - - productsMap.put(product.getId(), product); - return product; -} ----- - -This implementation demonstrates several key AtomicLong patterns: - -1. *Initialization*: `AtomicLong` is initialized with a starting value of 1 to avoid using 0 as a valid ID -2. *getAndIncrement*: Atomically returns the current value and increments it in one operation -3. *compareAndSet*: Safely updates the ID generator if a client provides a higher ID value, preventing ID collisions -4. *Retry Logic*: Uses a spinlock pattern for handling concurrent updates to the AtomicLong when needed - -The initialization of the idGenerator with a specific starting value ensures the IDs begin at a predictable value: - -[source,java] ----- -private final AtomicLong idGenerator = new AtomicLong(1); // Start IDs at 1 ----- - -This approach ensures that each product receives a unique ID without risk of duplicate IDs in a concurrent environment. - -Key benefits of this in-memory persistence approach: - -* *Simplicity*: No need for database configuration or ORM mapping -* *Performance*: Fast in-memory access without network or disk I/O -* *Thread Safety*: ConcurrentHashMap provides thread-safe operations without blocking -* *Scalability*: Suitable for containerized deployments - -==== Thread Safety Implementation Details - -The implementation ensures thread safety through multiple mechanisms: - -1. *ConcurrentHashMap*: Uses lock striping to allow concurrent reads and thread-safe writes -2. *AtomicLong*: Provides atomic operations for ID generation -3. *Immutable Returns*: Returns new collections rather than internal references: -+ -[source,java] ----- -// Returns a copy of the collection to prevent concurrent modification issues -public List findAllProducts() { - return new ArrayList<>(productsMap.values()); -} ----- - -4. *Atomic Operations*: Uses atomic map operations like `putIfAbsent` and `compute` where appropriate - -NOTE: This implementation is suitable for development, testing, and scenarios where persistence across restarts is not required. - -=== MicroProfile Config - -The application uses MicroProfile Config to externalize configuration: - -[source,properties] ----- -# Enable OpenAPI scanning -mp.openapi.scan=true - -# Maintenance mode configuration -product.maintenanceMode=false -product.maintenanceMessage=The product catalog service is currently in maintenance mode. Please try again later. ----- - -The maintenance mode configuration allows dynamic control of service availability: - -* `product.maintenanceMode` - When set to `true`, the service returns a 503 Service Unavailable response -* `product.maintenanceMessage` - Customizable message displayed when the service is in maintenance mode - -==== Maintenance Mode Implementation - -The service checks the maintenance mode configuration before processing requests: - -[source,java] ----- -@Inject -@ConfigProperty(name="product.maintenanceMode", defaultValue="false") -private boolean maintenanceMode; - -@Inject -@ConfigProperty(name="product.maintenanceMessage", - defaultValue="The product catalog service is currently in maintenance mode. Please try again later.") -private String maintenanceMessage; - -// In request handling method -if (maintenance.isMaintenanceMode()) { - return Response - .status(Response.Status.SERVICE_UNAVAILABLE) - .entity(maintenance.getMaintenanceMessage()) - .build(); -} ----- - -This pattern enables: - -* Graceful service degradation during maintenance periods -* Dynamic control without redeployment (when using external configuration sources) -* Clear communication to API consumers - -== Architecture - -The application follows a layered architecture pattern: - -* *REST Layer* (`ProductResource`) - Handles HTTP requests and responses -* *Service Layer* (`ProductService`) - Contains business logic -* *Repository Layer* (`ProductRepository`) - Manages data access with in-memory storage -* *Model Layer* (`Product`) - Represents the business entities - -=== Persistence Evolution - -This application originally used JPA with Derby for persistence, but has been refactored to use an in-memory implementation: - -[cols="1,1", options="header"] -|=== -| Original JPA/Derby | Current In-Memory Implementation -| Required database configuration | No database configuration needed -| Persistence across restarts | Data reset on restart -| Used EntityManager and transactions | Uses ConcurrentHashMap and AtomicLong -| Required datasource in server.xml | No datasource configuration required -| Complex error handling | Simplified error handling -|=== - -Key architectural benefits of this change: - -* *Simplified Deployment*: No external database required -* *Faster Startup*: No database initialization delay -* *Reduced Dependencies*: Fewer libraries and configurations -* *Easier Testing*: No test database setup needed -* *Consistent Development Environment*: Same behavior across all development machines - -=== Containerization with Docker - -The application can be packaged into a Docker container: - -[source,bash] ----- -# Build the application -mvn clean package - -# Build the Docker image -docker build -t catalog-service . - -# Run the container -docker run -d -p 5050:5050 --name catalog-service catalog-service ----- - -==== AtomicLong in Containerized Environments - -When running the application in Docker or Kubernetes, some important considerations about AtomicLong behavior: - -1. *Per-Container State*: Each container has its own AtomicLong instance and state -2. *ID Collisions in Scaling*: When running multiple replicas, IDs are only unique within each container -3. *Persistence and Restarts*: AtomicLong resets on container restart, potentially causing ID reuse - -To handle these issues in production multi-container environments: - -* *External ID Generation*: Consider using a distributed ID generator service -* *Database Sequences*: For database implementations, use database sequences -* *Universally Unique IDs*: Consider UUIDs instead of sequential numeric IDs -* *Centralized Counter Service*: Use Redis or other distributed counter - -Example of adapting the code for distributed environments: - -[source,java] ----- -// Using UUIDs for distributed environments -private String generateId() { - return UUID.randomUUID().toString(); -} ----- - -== Development Workflow - -=== Running Locally - -To run the application in development mode: - -[source,bash] ----- -mvn clean liberty:dev ----- - -This starts the server in development mode, which: - -* Automatically deploys your code changes -* Provides hot reload capability -* Enables a debugger on port 7777 - -== Project Structure - -[source] ----- -catalog/ -├── src/ -│ ├── main/ -│ │ ├── java/ -│ │ │ └── io/microprofile/tutorial/store/ -│ │ │ └── product/ -│ │ │ ├── entity/ # Domain entities -│ │ │ ├── resource/ # REST resources -│ │ │ └── ProductRestApplication.java -│ │ ├── liberty/ -│ │ │ └── config/ -│ │ │ └── server.xml # Liberty server configuration -│ │ ├── resources/ -│ │ │ └── META-INF/ -│ │ │ └── microprofile-config.properties -│ │ └── webapp/ # Web resources -│ │ ├── index.html # Landing page with API documentation -│ │ └── WEB-INF/ -│ │ └── web.xml # Web application configuration -│ └── test/ # Test classes -└── pom.xml # Maven build file ----- - -== Getting Started - -=== Prerequisites - -* JDK 17+ -* Maven 3.8+ - -=== Building and Running - -To build and run the application: - -[source,bash] ----- -# Clone the repository -git clone https://github.com/yourusername/liberty-rest-app.git -cd code/catalog - -# Build the application -mvn clean package - -# Run the application -mvn liberty:run ----- - -=== Testing the Application - -==== Testing MicroProfile Features - -[source,bash] ----- -# OpenAPI documentation -curl -X GET http://localhost:5050/openapi - -# Check if service is in maintenance mode -curl -X GET http://localhost:5050/api/products ----- - -To view the Swagger UI, open the following URL in your browser: -http://localhost:5050/openapi/ui - -To view the landing page with API documentation: -http://localhost:5050/ - -== Server Configuration - -The application uses the following Liberty server configuration: - -[source,xml] ----- - - - jakartaEE-10.0 - microProfile-6.1 - restfulWS - jsonp - jsonb - cdi - mpConfig - mpOpenAPI - - - - - ----- - -== Development - -=== Adding a New Endpoint - -To add a new endpoint: - -1. Create a new method in the `ProductResource` class -2. Add appropriate Jakarta Restful Web Service annotations -3. Add OpenAPI annotations for documentation -4. Implement the business logic - -Example: - -[source,java] ----- -@GET -@Path("/search") -@Produces(MediaType.APPLICATION_JSON) -@Operation(summary = "Search products", description = "Search products by name") -@APIResponses({ - @APIResponse(responseCode = "200", description = "Products matching search criteria") -}) -public Response searchProducts(@QueryParam("name") String name) { - List matchingProducts = products.stream() - .filter(p -> p.getName().toLowerCase().contains(name.toLowerCase())) - .collect(Collectors.toList()); - return Response.ok(matchingProducts).build(); -} ----- - -=== Performance Considerations - -The in-memory data store provides excellent performance for read operations, but there are important considerations: - -* *Memory Usage*: Large data sets may consume significant memory -* *Persistence*: Data is lost when the application restarts -* *Scalability*: In a multi-instance deployment, each instance will have its own data store - -For production scenarios requiring data persistence, consider: - -1. Adding a database layer (PostgreSQL, MongoDB, etc.) -2. Implementing a distributed cache (Hazelcast, Redis, etc.) -3. Adding data synchronization between instances - -=== Concurrency Implementation Details - -==== AtomicLong vs Synchronized Counter - -The repository uses `AtomicLong` rather than traditional synchronized counters: - -[cols="1,1", options="header"] -|=== -| Traditional Approach | AtomicLong Approach -| `private long counter = 0;` | `private final AtomicLong idGenerator = new AtomicLong(1);` -| `synchronized long getNextId() { return ++counter; }` | `long nextId = idGenerator.getAndIncrement();` -| Locks entire method | Lock-free operation -| Subject to contention | Uses CPU compare-and-swap -| Performance degrades with multiple threads | Maintains performance under concurrency -|=== - -==== AtomicLong vs Other Concurrency Options - -[cols="1,1,1,1", options="header"] -|=== -| Feature | AtomicLong | Synchronized | java.util.concurrent.locks.Lock -| Type | Non-blocking | Intrinsic lock | Explicit lock -| Granularity | Single variable | Method/block | Customizable -| Performance under contention | High | Lower | Medium -| Visibility guarantee | Yes | Yes | Yes -| Atomicity guarantee | Yes | Yes | Yes -| Fairness policy | No | No | Optional -| Try/timeout support | Yes (compareAndSet) | No | Yes -| Multiple operations atomicity | Limited | Yes | Yes -| Implementation complexity | Simple | Simple | Complex -|=== - -===== When to Choose AtomicLong - -* *High-Contention Scenarios*: When many threads need to access/modify a counter -* *Single Variable Operations*: When only one variable needs atomic operations -* *Performance-Critical Code*: When minimizing lock contention is essential -* *Read-Heavy Workloads*: When reads significantly outnumber writes - -For this in-memory product repository, AtomicLong provides an optimal balance of safety and performance. - -==== Implementation in createProduct Method - -The ID generation logic handles both automatic and manual ID assignment: - -[source,java] ----- -public Product createProduct(Product product) { - // Generate ID if not provided - if (product.getId() == null) { - product.setId(idGenerator.getAndIncrement()); - } else { - // Update idGenerator if the provided ID is greater than current - long nextId = product.getId() + 1; - while (true) { - long currentId = idGenerator.get(); - if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { - break; - } - } - } - - productsMap.put(product.getId(), product); - return product; -} ----- - -This implementation ensures ID integrity while supporting both system-generated and client-provided IDs. - -This enables scanning of OpenAPI annotations in the application. - -== Troubleshooting - -=== Common Issues - -* *OpenAPI documentation not available*: Make sure `mp.openapi.scan=true` is set in the properties file -* *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations -* *Service always in maintenance mode*: Check the `product.maintenanceMode` property in `microprofile-config.properties` -* *API returning 503 responses*: The service is likely in maintenance mode; set `product.maintenanceMode=false` in configuration -* *OpenAPI documentation not available*: Make sure `mp.openapi.scan=true` is set in the properties file -* *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations - -=== Thread Safety Troubleshooting - -If experiencing concurrency issues: - -1. *Verify AtomicLong Usage*: Ensure all ID generation uses `AtomicLong.getAndIncrement()` instead of manual increment -2. *Check Collection Returns*: Always return copies of collections, not direct references: -+ -[source,java] ----- -public List findAllProducts() { - return new ArrayList<>(productsMap.values()); // Correct: returns a new copy -} ----- - -3. *Use ConcurrentHashMap Methods*: Prefer atomic methods like `compute`, `computeIfAbsent`, or `computeIfPresent` for complex operations -4. *Avoid Iteration + Modification*: Don't modify the map while iterating over it - -=== Understanding AtomicLong Internals - -If you need to debug issues with AtomicLong, understanding its internal mechanisms is helpful: - -==== Compare-And-Swap Operation - -AtomicLong relies on hardware-level atomic instructions, specifically Compare-And-Swap (CAS): - -[source,text] ----- -function CAS(address, expected, new): - atomically: - if memory[address] == expected: - memory[address] = new - return true - else: - return false ----- - -The implementation of `getAndIncrement()` uses this mechanism: - -[source,java] ----- -// Simplified implementation of getAndIncrement -public long getAndIncrement() { - while (true) { - long current = get(); - long next = current + 1; - if (compareAndSet(current, next)) - return current; - } -} ----- - -==== Memory Ordering and Visibility - -AtomicLong ensures that memory visibility follows the Java Memory Model: - -* All writes to the AtomicLong by one thread are visible to reads from other threads -* Memory barriers are established when performing atomic operations -* Volatile semantics are guaranteed without using the volatile keyword - -==== Diagnosing AtomicLong Issues - -1. *Unexpected ID Values*: Check for manual ID assignment bypassing the AtomicLong -2. *Duplicate IDs*: Verify the initialization value and ensure all ID assignments go through AtomicLong -3. *Performance Issues*: Look for excessive contention (many threads updating simultaneously) - -=== Logs - -Server logs can be found at: - -[source] ----- -target/liberty/wlp/usr/servers/defaultServer/logs/ ----- - -== Resources - -* https://microprofile.io/[MicroProfile] - -=== HTML Landing Page - -The application includes a user-friendly HTML landing page (`index.html`) that provides: - -* Service overview with comprehensive documentation -* API endpoints documentation with methods and descriptions -* Interactive examples for all API operations -* Links to OpenAPI/Swagger documentation - -==== Maintenance Mode Configuration in the UI - -The index.html page is designed to work seamlessly with the maintenance mode configuration. When maintenance mode is enabled via the `product.maintenanceMode` property, all API endpoints return a 503 Service Unavailable response with the configured maintenance message. - -The landing page displays comprehensive documentation about the API regardless of the maintenance state, allowing developers to continue learning about the API even when the service is undergoing maintenance. - -Key features of the landing page: - -* *Responsive Design*: Works well on desktop and mobile devices -* *Comprehensive API Documentation*: All endpoints with sample requests and responses -* *Interactive Examples*: Detailed sample requests and responses for each endpoint -* *Modern Styling*: Clean, professional appearance with card-based layout - -The landing page is configured as the welcome file in `web.xml`: - -[source,xml] ----- - - index.html - ----- - -This provides a user-friendly entry point for API consumers and developers. - - diff --git a/code/chapter11/catalog/pom.xml b/code/chapter11/catalog/pom.xml deleted file mode 100644 index 853bfdc1..00000000 --- a/code/chapter11/catalog/pom.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - 4.0.0 - - io.microprofile.tutorial - catalog - 1.0-SNAPSHOT - war - - - - - 17 - 17 - - UTF-8 - UTF-8 - - - 5050 - 5051 - - catalog - - - - - - - org.projectlombok - lombok - 1.18.26 - provided - - - - - jakarta.platform - jakarta.jakartaee-api - 10.0.0 - provided - - - - - org.eclipse.microprofile - microprofile - 6.1 - pom - provided - - - - - ${project.artifactId} - - - - io.openliberty.tools - liberty-maven-plugin - 3.11.2 - - mpServer - - - - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - - - - \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java deleted file mode 100644 index 9759e1f7..00000000 --- a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.microprofile.tutorial.store.product; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -@ApplicationPath("/api") -public class ProductRestApplication extends Application { - // No additional configuration is needed here -} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java deleted file mode 100644 index 84e3b23e..00000000 --- a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.microprofile.tutorial.store.product.entity; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Product { - - private Long id; - private String name; - private String description; - private Double price; -} diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java deleted file mode 100644 index 6631fdea..00000000 --- a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java +++ /dev/null @@ -1,138 +0,0 @@ -package io.microprofile.tutorial.store.product.repository; - -import io.microprofile.tutorial.store.product.entity.Product; -import jakarta.enterprise.context.ApplicationScoped; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * Repository class for Product entity. - * Provides in-memory persistence operations using ConcurrentHashMap. - */ -@ApplicationScoped -public class ProductRepository { - - private static final Logger LOGGER = Logger.getLogger(ProductRepository.class.getName()); - - // In-memory storage using ConcurrentHashMap for thread safety - private final Map productsMap = new ConcurrentHashMap<>(); - - // ID generator - private final AtomicLong idGenerator = new AtomicLong(1); - - /** - * Constructor with sample data initialization. - */ - public ProductRepository() { - // Initialize with sample products - createProduct(new Product(null, "iPhone", "Apple iPhone 15", 999.99)); - createProduct(new Product(null, "MacBook", "Apple MacBook Air", 1299.0)); - createProduct(new Product(null, "iPad", "Apple iPad Pro", 799.0)); - LOGGER.info("ProductRepository initialized with sample products"); - } - - /** - * Retrieves all products. - * - * @return List of all products - */ - public List findAllProducts() { - LOGGER.fine("Repository: Finding all products"); - return new ArrayList<>(productsMap.values()); - } - - /** - * Retrieves a product by ID. - * - * @param id Product ID - * @return The product or null if not found - */ - public Product findProductById(Long id) { - LOGGER.fine("Repository: Finding product with ID: " + id); - return productsMap.get(id); - } - - /** - * Creates a new product. - * - * @param product Product data to create - * @return The created product with ID - */ - public Product createProduct(Product product) { - // Generate ID if not provided - if (product.getId() == null) { - product.setId(idGenerator.getAndIncrement()); - } else { - // Update idGenerator if the provided ID is greater than current - long nextId = product.getId() + 1; - while (true) { - long currentId = idGenerator.get(); - if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { - break; - } - } - } - - LOGGER.fine("Repository: Creating product with ID: " + product.getId()); - productsMap.put(product.getId(), product); - return product; - } - - /** - * Updates an existing product. - * - * @param product Updated product data - * @return The updated product or null if not found - */ - public Product updateProduct(Product product) { - Long id = product.getId(); - if (id != null && productsMap.containsKey(id)) { - LOGGER.fine("Repository: Updating product with ID: " + id); - productsMap.put(id, product); - return product; - } - LOGGER.warning("Repository: Product not found for update, ID: " + id); - return null; - } - - /** - * Deletes a product by ID. - * - * @param id ID of the product to delete - * @return true if deleted, false if not found - */ - public boolean deleteProduct(Long id) { - if (productsMap.containsKey(id)) { - LOGGER.fine("Repository: Deleting product with ID: " + id); - productsMap.remove(id); - return true; - } - LOGGER.warning("Repository: Product not found for deletion, ID: " + id); - return false; - } - - /** - * Searches for products by criteria. - * - * @param name Product name (optional) - * @param description Product description (optional) - * @param minPrice Minimum price (optional) - * @param maxPrice Maximum price (optional) - * @return List of matching products - */ - public List searchProducts(String name, String description, Double minPrice, Double maxPrice) { - LOGGER.fine("Repository: Searching for products with criteria"); - - return productsMap.values().stream() - .filter(p -> name == null || p.getName().toLowerCase().contains(name.toLowerCase())) - .filter(p -> description == null || p.getDescription().toLowerCase().contains(description.toLowerCase())) - .filter(p -> minPrice == null || p.getPrice() >= minPrice) - .filter(p -> maxPrice == null || p.getPrice() <= maxPrice) - .collect(Collectors.toList()); - } -} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java deleted file mode 100644 index 316ac882..00000000 --- a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java +++ /dev/null @@ -1,182 +0,0 @@ -package io.microprofile.tutorial.store.product.resource; - -import java.util.List; -import java.util.logging.Logger; -import java.util.logging.Level; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import io.microprofile.tutorial.store.product.entity.Product; -import io.microprofile.tutorial.store.product.service.ProductService; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -@ApplicationScoped -@Path("/products") -@Tag(name = "Product Resource", description = "CRUD operations for products") -public class ProductResource { - - private static final Logger LOGGER = Logger.getLogger(ProductResource.class.getName()); - - @Inject - @ConfigProperty(name="product.maintenanceMode", defaultValue="false") - private boolean maintenanceMode; - - @Inject - private ProductService productService; - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "List all products", description = "Retrieves a list of all products") - @APIResponses({ - @APIResponse( - responseCode = "200", - description = "Successful, list of products found", - content = @Content(mediaType = "application/json", - schema = @Schema(implementation = Product.class))), - @APIResponse( - responseCode = "400", - description = "Unsuccessful, no products found", - content = @Content(mediaType = "application/json") - ), - @APIResponse( - responseCode = "503", - description = "Service is under maintenance", - content = @Content(mediaType = "application/json") - ) - }) - public Response getAllProducts() { - LOGGER.log(Level.INFO, "REST: Fetching all products"); - List products = productService.findAllProducts(); - - if (maintenanceMode) { - return Response - .status(Response.Status.SERVICE_UNAVAILABLE) - .entity("Service is under maintenance") - .build(); - } - - if (products != null && !products.isEmpty()) { - return Response - .status(Response.Status.OK) - .entity(products).build(); - } else { - return Response - .status(Response.Status.NOT_FOUND) - .entity("No products found") - .build(); - } - } - - @GET - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Get product by ID", description = "Returns a product by its ID") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Product found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))), - @APIResponse(responseCode = "404", description = "Product not found"), - @APIResponse(responseCode = "503", description = "Service is under maintenance") - }) - public Response getProductById(@PathParam("id") Long id) { - LOGGER.log(Level.INFO, "REST: Fetching product with id: {0}", id); - - if (maintenanceMode) { - return Response - .status(Response.Status.SERVICE_UNAVAILABLE) - .entity("Service is under maintenance") - .build(); - } - - Product product = productService.findProductById(id); - if (product != null) { - return Response.ok(product).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Create a new product", description = "Creates a new product") - @APIResponses({ - @APIResponse(responseCode = "201", description = "Product created", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))) - }) - public Response createProduct(Product product) { - LOGGER.info("REST: Creating product: " + product); - Product createdProduct = productService.createProduct(product); - return Response.status(Response.Status.CREATED).entity(createdProduct).build(); - } - - @PUT - @Path("/{id}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Update a product", description = "Updates an existing product by its ID") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Product updated", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))), - @APIResponse(responseCode = "404", description = "Product not found") - }) - public Response updateProduct(@PathParam("id") Long id, Product updatedProduct) { - LOGGER.info("REST: Updating product with id: " + id); - Product updated = productService.updateProduct(id, updatedProduct); - if (updated != null) { - return Response.ok(updated).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - - @DELETE - @Path("/{id}") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Delete a product", description = "Deletes a product by its ID") - @APIResponses({ - @APIResponse(responseCode = "204", description = "Product deleted"), - @APIResponse(responseCode = "404", description = "Product not found") - }) - public Response deleteProduct(@PathParam("id") Long id) { - LOGGER.info("REST: Deleting product with id: " + id); - boolean deleted = productService.deleteProduct(id); - if (deleted) { - return Response.noContent().build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - } - - @GET - @Path("/search") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Search products", description = "Search products by criteria") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Search results", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))) - }) - public Response searchProducts( - @QueryParam("name") String name, - @QueryParam("description") String description, - @QueryParam("minPrice") Double minPrice, - @QueryParam("maxPrice") Double maxPrice) { - LOGGER.info("REST: Searching products with criteria"); - List results = productService.searchProducts(name, description, minPrice, maxPrice); - return Response.ok(results).build(); - } -} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java deleted file mode 100644 index 804fd920..00000000 --- a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.microprofile.tutorial.store.product.service; - -import io.microprofile.tutorial.store.product.entity.Product; -import io.microprofile.tutorial.store.product.repository.ProductRepository; -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import java.util.List; -import java.util.logging.Logger; - -/** - * Service class for Product operations. - * Contains business logic for product management. - */ -@RequestScoped -public class ProductService { - - private static final Logger LOGGER = Logger.getLogger(ProductService.class.getName()); - - @Inject - private ProductRepository repository; - - /** - * Retrieves all products. - * - * @return List of all products - */ - public List findAllProducts() { - LOGGER.info("Service: Finding all products"); - return repository.findAllProducts(); - } - - /** - * Retrieves a product by ID. - * - * @param id Product ID - * @return The product or null if not found - */ - public Product findProductById(Long id) { - LOGGER.info("Service: Finding product with ID: " + id); - return repository.findProductById(id); - } - - /** - * Creates a new product. - * - * @param product Product data to create - * @return The created product with ID - */ - public Product createProduct(Product product) { - LOGGER.info("Service: Creating new product: " + product); - return repository.createProduct(product); - } - - /** - * Updates an existing product. - * - * @param id ID of the product to update - * @param updatedProduct Updated product data - * @return The updated product or null if not found - */ - public Product updateProduct(Long id, Product updatedProduct) { - LOGGER.info("Service: Updating product with ID: " + id); - - Product existingProduct = repository.findProductById(id); - if (existingProduct != null) { - // Set the ID to ensure correct update - updatedProduct.setId(id); - return repository.updateProduct(updatedProduct); - } - return null; - } - - /** - * Deletes a product by ID. - * - * @param id ID of the product to delete - * @return true if deleted, false if not found - */ - public boolean deleteProduct(Long id) { - LOGGER.info("Service: Deleting product with ID: " + id); - return repository.deleteProduct(id); - } - - /** - * Searches for products by criteria. - * - * @param name Product name (optional) - * @param description Product description (optional) - * @param minPrice Minimum price (optional) - * @param maxPrice Maximum price (optional) - * @return List of matching products - */ - public List searchProducts(String name, String description, Double minPrice, Double maxPrice) { - LOGGER.info("Service: Searching for products with criteria"); - return repository.searchProducts(name, description, minPrice, maxPrice); - } -} diff --git a/code/chapter11/catalog/src/main/liberty/config/server.xml b/code/chapter11/catalog/src/main/liberty/config/server.xml deleted file mode 100644 index 8fec0a65..00000000 --- a/code/chapter11/catalog/src/main/liberty/config/server.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - jakartaEE-10.0 - microProfile-6.1 - restfulWS - jsonp - jsonb - cdi - mpConfig - mpOpenAPI - mpHealth - mpMetrics - mpTelemetry - mpFaultTolerance - mpJwt - mpRestClient - - - - \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index 03fbb4d9..00000000 --- a/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,5 +0,0 @@ -# microprofile-config.properties -product.maintainenceMode=false - -# Enable OpenAPI scanning -mp.openapi.scan=true \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml b/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 10105161..00000000 --- a/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - Product Catalog Service - - - index.html - - - diff --git a/code/chapter11/catalog/src/main/webapp/index.html b/code/chapter11/catalog/src/main/webapp/index.html deleted file mode 100644 index 54622a43..00000000 --- a/code/chapter11/catalog/src/main/webapp/index.html +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - Product Catalog Service - - - -
-

Product Catalog Service

-

A microservice for managing product information in the e-commerce platform

-
- -
-
-

Service Overview

-

The Product Catalog Service provides a REST API for managing product information, including:

-
    -
  • Creating new products
  • -
  • Retrieving product details
  • -
  • Updating existing products
  • -
  • Deleting products
  • -
  • Searching for products by various criteria
  • -
-
-

MicroProfile Config Implementation

-

This service implements configurability as per MicroProfile Config standards. Key configuration properties include:

-
    -
  • product.maintenanceMode - Controls whether the service is in maintenance mode (returns 503 responses)
  • -
  • mp.openapi.scan - Enables automatic OpenAPI documentation generation
  • -
-

MicroProfile Config allows these properties to be changed via environment variables, system properties, or configuration files without requiring application redeployment.

-
-
- -
-

API Endpoints

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OperationMethodURLDescription
List All ProductsGET/api/productsRetrieves a list of all products
Get Product by IDGET/api/products/{id}Returns a product by its ID
Create ProductPOST/api/productsCreates a new product
Update ProductPUT/api/products/{id}Updates an existing product by its ID
Delete ProductDELETE/api/products/{id}Deletes a product by its ID
Search ProductsGET/api/products/searchSearch products by criteria (name, description, price range)
-
- -
-

API Documentation

-

The API is documented using MicroProfile OpenAPI. You can access the Swagger UI at:

-

/openapi/ui

-

The OpenAPI definition is available at:

-

/openapi

-
- -
-

Sample Usage

- -

List All Products

-
GET /api/products
-

Response:

-
[
-  {
-    "id": 1,
-    "name": "Smartphone X",
-    "description": "Latest smartphone with advanced features",
-    "price": 799.99
-  },
-  {
-    "id": 2,
-    "name": "Laptop Pro",
-    "description": "High-performance laptop for professionals",
-    "price": 1299.99
-  }
-]
- -

Get Product by ID

-
GET /api/products/1
-

Response:

-
{
-  "id": 1,
-  "name": "Smartphone X",
-  "description": "Latest smartphone with advanced features",
-  "price": 799.99
-}
- -

Create a New Product

-
POST /api/products
-Content-Type: application/json
-
-{
-  "name": "Wireless Earbuds",
-  "description": "Premium wireless earbuds with noise cancellation",
-  "price": 149.99
-}
-

Response:

-
{
-  "id": 3,
-  "name": "Wireless Earbuds",
-  "description": "Premium wireless earbuds with noise cancellation",
-  "price": 149.99
-}
- -

Update a Product

-
PUT /api/products/3
-Content-Type: application/json
-
-{
-  "name": "Wireless Earbuds Pro",
-  "description": "Premium wireless earbuds with advanced noise cancellation",
-  "price": 179.99
-}
-

Response:

-
{
-  "id": 3,
-  "name": "Wireless Earbuds Pro",
-  "description": "Premium wireless earbuds with advanced noise cancellation",
-  "price": 179.99
-}
- -

Delete a Product

-
DELETE /api/products/3
-

Response: No content (204)

- -

Search for Products

-
GET /api/products/search?name=laptop&minPrice=1000&maxPrice=2000
-

Response:

-
[
-  {
-    "id": 2,
-    "name": "Laptop Pro",
-    "description": "High-performance laptop for professionals",
-    "price": 1299.99
-  }
-]
-
-
- -
-

Product Catalog Service

-

© 2025 - MicroProfile APT Tutorial

-
- - - - diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java similarity index 100% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java similarity index 97% rename from code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java rename to code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java index ae8a9416..3ee718c0 100644 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java +++ b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java @@ -8,7 +8,6 @@ import io.microprofile.tutorial.store.inventory.dto.Product; import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; -import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -52,16 +51,15 @@ public boolean isProductAvailable(Long productId) { LOGGER.fine("Checking product availability for ID: " + productId); try { - // Demonstrate RestClientBuilder usage - build a REST client programmatically - URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); - + // Demonstrate RestClientBuilder usage with MP Rest Client 4.0 + // Using new baseUri(String) method - no need for URI.create() ProductServiceClient dynamicClient = RestClientBuilder.newBuilder() - .baseUri(catalogServiceUri) + .baseUri("http://localhost:5050/catalog/api") .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(ProductServiceClient.class); - LOGGER.fine("Built dynamic REST client for catalog service at: " + catalogServiceUri); + LOGGER.fine("Built dynamic REST client for catalog service using baseUri(String)"); Product product = dynamicClient.getProductById(productId); boolean available = product != null; @@ -460,6 +458,7 @@ public Inventory reserveInventory(Long productId, int quantityToReserve) { /** * Demonstrates advanced RestClientBuilder usage with custom configuration. * This method builds a REST client with specific timeout and error handling settings. + * Uses MicroProfile Rest Client 4.0 baseUri(String) convenience method. * * @param productId The product ID to check * @return Product details if found, null otherwise @@ -468,11 +467,10 @@ public Product getProductWithCustomClient(Long productId) { LOGGER.info("Getting product details using custom RestClientBuilder for ID: " + productId); try { - // Build REST client with custom configuration - URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); - + // Build REST client with custom configuration using MP Rest Client 4.0 + // Using baseUri(String) - no URI.create() needed ProductServiceClient customClient = RestClientBuilder.newBuilder() - .baseUri(catalogServiceUri) + .baseUri("http://localhost:5050/catalog/api") .connectTimeout(3, TimeUnit.SECONDS) // Custom connect timeout .readTimeout(8, TimeUnit.SECONDS) // Custom read timeout .build(ProductServiceClient.class); diff --git a/code/chapter11/shoppingcart/Dockerfile b/code/chapter11/chapter11/shoppingcart/Dockerfile similarity index 100% rename from code/chapter11/shoppingcart/Dockerfile rename to code/chapter11/chapter11/shoppingcart/Dockerfile diff --git a/code/chapter11/shoppingcart/README.md b/code/chapter11/chapter11/shoppingcart/README.md similarity index 100% rename from code/chapter11/shoppingcart/README.md rename to code/chapter11/chapter11/shoppingcart/README.md diff --git a/code/chapter11/shoppingcart/pom.xml b/code/chapter11/chapter11/shoppingcart/pom.xml similarity index 95% rename from code/chapter11/shoppingcart/pom.xml rename to code/chapter11/chapter11/shoppingcart/pom.xml index 9451fea0..df9eae09 100644 --- a/code/chapter11/shoppingcart/pom.xml +++ b/code/chapter11/chapter11/shoppingcart/pom.xml @@ -14,11 +14,11 @@ UTF-8 - 17 + 21 10.0.0 - 6.1 - 23.0.0.3 - 1.18.24 + 7.1 + 25.0.0.1 + 1.18.36 diff --git a/code/chapter11/shoppingcart/run-docker.sh b/code/chapter11/chapter11/shoppingcart/run-docker.sh old mode 100755 new mode 100644 similarity index 100% rename from code/chapter11/shoppingcart/run-docker.sh rename to code/chapter11/chapter11/shoppingcart/run-docker.sh diff --git a/code/chapter11/shoppingcart/run.sh b/code/chapter11/chapter11/shoppingcart/run.sh old mode 100755 new mode 100644 similarity index 100% rename from code/chapter11/shoppingcart/run.sh rename to code/chapter11/chapter11/shoppingcart/run.sh diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java similarity index 100% rename from code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java rename to code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java diff --git a/code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties similarity index 100% rename from code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties rename to code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties diff --git a/code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml b/code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml rename to code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml diff --git a/code/chapter11/shoppingcart/src/main/webapp/index.html b/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html similarity index 100% rename from code/chapter11/shoppingcart/src/main/webapp/index.html rename to code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html diff --git a/code/chapter11/shoppingcart/src/main/webapp/index.jsp b/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp similarity index 100% rename from code/chapter11/shoppingcart/src/main/webapp/index.jsp rename to code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp diff --git a/code/chapter11/docker-compose.yml b/code/chapter11/docker-compose.yml deleted file mode 100644 index c52b6af0..00000000 --- a/code/chapter11/docker-compose.yml +++ /dev/null @@ -1,85 +0,0 @@ -services: - user-service: - build: ./user - ports: - - "6050:6050" - - "6051:6051" - networks: - - ecommerce-network - environment: - - INVENTORY_SERVICE_URL=http://inventory-service:7050 - - ORDER_SERVICE_URL=http://order-service:8050 - - CATALOG_SERVICE_URL=http://catalog-service:9050 - - inventory-service: - build: ./inventory - ports: - - "7050:7050" - - "7051:7051" - networks: - - ecommerce-network - depends_on: - - user-service - - order-service: - build: ./order - ports: - - "8050:8050" - - "8051:8051" - networks: - - ecommerce-network - depends_on: - - user-service - - inventory-service - - catalog-service: - build: ./catalog - ports: - - "5050:5050" - - "5051:5051" - networks: - - ecommerce-network - depends_on: - - inventory-service - - payment-service: - build: ./payment - ports: - - "9050:9050" - - "9051:9051" - networks: - - ecommerce-network - depends_on: - - user-service - - order-service - - shoppingcart-service: - build: ./shoppingcart - ports: - - "4050:4050" - - "4051:4051" - networks: - - ecommerce-network - depends_on: - - inventory-service - - catalog-service - environment: - - INVENTORY_SERVICE_URL=http://inventory-service:7050 - - CATALOG_SERVICE_URL=http://catalog-service:5050 - - shipment-service: - build: ./shipment - ports: - - "8060:8060" - - "9060:9060" - networks: - - ecommerce-network - depends_on: - - order-service - environment: - - ORDER_SERVICE_URL=http://order-service:8050/order - - MP_CONFIG_PROFILE=docker - -networks: - ecommerce-network: - driver: bridge diff --git a/code/chapter11/inventory/README.adoc b/code/chapter11/inventory/README.adoc deleted file mode 100644 index 2df7fec7..00000000 --- a/code/chapter11/inventory/README.adoc +++ /dev/null @@ -1,387 +0,0 @@ -= Inventory Service -:toc: left -:icons: font -:source-highlighter: highlightjs - -A comprehensive Jakarta EE and MicroProfile-based REST service for inventory management demonstrating advanced MicroProfile Rest Client integration patterns. - -== Overview - -The Inventory Service is a production-ready microservice built with Jakarta EE 10.0 and MicroProfile 6.1, showcasing comprehensive REST client integration patterns with the Catalog Service. This service demonstrates three different approaches to MicroProfile Rest Client usage: - -* **Injected REST Client** (`@RestClient`) for standard operations -* **RestClientBuilder** with custom timeouts for availability checks -* **Advanced RestClientBuilder** with fine-tuned configuration for detailed operations - -== Key Features - -=== Core Functionality -* Complete CRUD operations for inventory management -* Product validation against catalog service -* Inventory reservation system with availability checks -* Bulk operations support -* Enriched inventory data with product information -* Pagination and filtering capabilities - -=== MicroProfile Rest Client Integration -* **Three distinct REST client approaches** for different use cases -* **Product validation** before inventory operations -* **Service integration** with catalog service on port 5050 -* **Error handling** for non-existent products and service failures -* **Timeout configurations** optimized for different operation types - -=== Advanced Features -* Bean validation for input data -* Comprehensive exception handling -* Transaction management for atomic operations -* OpenAPI documentation with Swagger UI -* Health checks and service monitoring - -== Running the Application - -To start the application, run: - -[source,bash] ----- -cd inventory -mvn liberty:run ----- - -This will start the Open Liberty server on port 7050 (HTTP) and 7051 (HTTPS). - -== MicroProfile Rest Client Implementations - -=== 1. Injected REST Client (`@RestClient`) -Used for standard product validation operations: - -[source,java] ----- -@Inject -@RestClient -private ProductServiceClient productServiceClient; ----- - -**Configuration** (microprofile-config.properties): -[source,properties] ----- -product-service/mp-rest/url=http://localhost:5050/catalog/api -product-service/mp-rest/scope=jakarta.inject.Singleton -product-service/mp-rest/connectTimeout=5000 -product-service/mp-rest/readTimeout=10000 ----- - -=== 2. RestClientBuilder (5s/10s timeout) -Used for lightweight availability checks during reservation: - -[source,java] ----- -ProductServiceClient dynamicClient = RestClientBuilder.newBuilder() - .baseUri(catalogServiceUri) - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(ProductServiceClient.class); ----- - -=== 3. Advanced RestClientBuilder (3s/8s timeout) -Used for detailed product information retrieval: - -[source,java] ----- -ProductServiceClient customClient = RestClientBuilder.newBuilder() - .baseUri(catalogServiceUri) - .connectTimeout(3, TimeUnit.SECONDS) - .readTimeout(8, TimeUnit.SECONDS) - .build(ProductServiceClient.class); ----- - -== Complete API Endpoints - -[cols="1,3,2,3", options="header"] -|=== -|Method |URL |MicroProfile Client |Description - -|GET -|/api/inventories -|None -|Get all inventory items with pagination/filtering - -|POST -|/api/inventories -|@RestClient -|Create new inventory (validates product exists) - -|GET -|/api/inventories/{id} -|None -|Get inventory by ID - -|PUT -|/api/inventories/{id} -|@RestClient -|Update inventory (validates product exists) - -|DELETE -|/api/inventories/{id} -|None -|Delete inventory - -|GET -|/api/inventories/product/{productId} -|None -|Get inventory by product ID - -|PATCH -|/api/inventories/product/{productId}/quantity/{quantity} -|None -|Update product quantity - -|PATCH -|/api/inventories/product/{productId}/reserve/{quantity} -|RestClientBuilder (5s/10s) -|Reserve inventory with availability check - -|GET -|/api/inventories/product-info/{productId} -|Advanced RestClientBuilder (3s/8s) -|Get product details using custom client - -|GET -|/api/inventories/{id}/with-product-info -|@RestClient -|Get enriched inventory with product information - -|POST -|/api/inventories/bulk -|@RestClient -|Bulk create inventories with validation -|=== - -== Service Integration - -=== Catalog Service Integration -The inventory service integrates with the catalog service running on port 5050 to: - -* **Validate products** before creating or updating inventory -* **Check product availability** during reservation operations -* **Enrich inventory data** with product details (name, description, price) -* **Handle service failures** gracefully with appropriate error responses - -=== Error Handling -* **404 responses** when products don't exist in catalog -* **Service timeout handling** with different timeout configurations per operation -* **Fallback behavior** for service communication failures -* **Validation errors** for invalid inventory data - -== Testing with cURL - -=== Basic Operations - -==== Get all inventory items -[source,bash] ----- -curl -X GET http://localhost:7050/inventory/api/inventories ----- - -==== Get inventory by ID -[source,bash] ----- -curl -X GET http://localhost:7050/inventory/api/inventories/1 ----- - -==== Create new inventory (with product validation) -[source,bash] ----- -curl -X POST http://localhost:7050/inventory/api/inventories \ - -H "Content-Type: application/json" \ - -d '{"productId": 1, "quantity": 50, "location": "Warehouse A"}' ----- - -==== Update inventory (with product validation) -[source,bash] ----- -curl -X PUT http://localhost:7050/inventory/api/inventories/1 \ - -H "Content-Type: application/json" \ - -d '{"productId": 1, "quantity": 75, "location": "Warehouse B"}' ----- - -==== Delete inventory -[source,bash] ----- -curl -X DELETE http://localhost:7050/inventory/api/inventories/1 ----- - -=== Advanced Operations - -==== Reserve inventory (uses RestClientBuilder) -[source,bash] ----- -curl -X PATCH http://localhost:7050/inventory/api/inventories/product/1/reserve/10 ----- - -==== Get product info (uses Advanced RestClientBuilder) -[source,bash] ----- -curl -X GET http://localhost:7050/inventory/api/inventories/product-info/1 ----- - -==== Get enriched inventory with product details -[source,bash] ----- -curl -X GET http://localhost:7050/inventory/api/inventories/1/with-product-info ----- - -==== Bulk create inventories -[source,bash] ----- -curl -X POST http://localhost:7050/inventory/api/inventories/bulk \ - -H "Content-Type: application/json" \ - -d '[ - {"productId": 1, "quantity": 50, "location": "Warehouse A"}, - {"productId": 2, "quantity": 30, "location": "Warehouse B"} - ]' ----- - -==== Get inventory with pagination and filtering -[source,bash] ----- - -# Filter by location -curl -X GET "http://localhost:6050/inventory/api/inventories?location=Warehouse%20A" - -# Filter by minimum quantity -curl -X GET "http://localhost:6050/inventory/api/inventories?minQuantity=10" ----- - -== Test Scripts - -Comprehensive test scripts are available to test all functionality: - -* **`test-inventory-endpoints.sh`** - Complete test suite covering all endpoints and MicroProfile Rest Client features -* **`quick-test-commands.sh`** - Quick reference commands for manual testing -* **`TEST-SCRIPTS-README.md`** - Detailed documentation of test scenarios and expected responses - -[source,bash] ----- -# Run comprehensive test suite -./test-inventory-endpoints.sh - -# View test documentation -cat TEST-SCRIPTS-README.md ----- - -== Configuration - -=== MicroProfile Config Properties - -**REST Client Configuration** (`microprofile-config.properties`): -[source,properties] ----- -# Injected REST Client configuration -product-service/mp-rest/url=http://localhost:5050/catalog/api -product-service/mp-rest/scope=jakarta.inject.Singleton -product-service/mp-rest/connectTimeout=5000 -product-service/mp-rest/readTimeout=10000 -product-service/mp-rest/followRedirects=true ----- - -**RestClientBuilder Configuration** (programmatic): -[source,java] ----- -# Availability check client (5s/10s timeout) -URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); - -# Product info client (3s/8s timeout) -URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); ----- - -== OpenAPI Documentation - -View the complete API documentation: - -* **Swagger UI**: http://localhost:7050/inventory/api/openapi-ui/ -* **OpenAPI JSON**: http://localhost:7050/inventory/api/openapi -* **Service Landing Page**: http://localhost:7050/inventory/ - -== Project Structure - -[source] ----- -inventory/ -├── src/ -│ └── main/ -│ ├── java/ # Java source files -│ │ └── io/microprofile/tutorial/store/inventory/ -│ │ ├── entity/ # Domain entities -│ │ ├── exception/ # Custom exceptions -│ │ ├── service/ # Business logic -│ │ └── resource/ # REST endpoints -│ ├── liberty/ -│ │ └── config/ # Liberty server configuration -│ └── webapp/ # Web resources -└── pom.xml # Project dependencies and build ----- - -== Exception Handling - -The service implements a robust exception handling mechanism: - -[cols="1,2", options="header"] -|=== -|Exception |Purpose - -|`InventoryNotFoundException` -|Thrown when requested inventory item does not exist (HTTP 404) - -|`InventoryConflictException` -|Thrown when attempting to create duplicate inventory (HTTP 409) -|=== - -Exceptions are handled globally using `@Provider`: - -[source,java] ----- -@Provider -public class InventoryExceptionMapper implements ExceptionMapper { - // Maps exceptions to appropriate HTTP responses -} ----- - -== Transaction Management - -The service includes the Jakarta Transactions feature (`transaction-1.3`) but does not use database persistence. In this context, `@Transactional` has limited use: - -* Can be used for transaction-like behavior in memory operations -* Useful when you need to ensure multiple operations are executed atomically -* Provides rollback capability for in-memory state changes -* Primarily used for maintaining consistency in distributed operations - -[NOTE] -==== -Since this service doesn't use database persistence, `@Transactional` mainly serves as a boundary for: - -* Coordinating multiple service method calls -* Managing concurrent access to shared resources -* Ensuring atomic operations across multiple steps -==== - -Example usage: - -[source,java] ----- -@ApplicationScoped -public class InventoryService { - private final ConcurrentHashMap inventoryStore; - - @Transactional - public void updateInventory(Long id, Inventory inventory) { - // Even without persistence, @Transactional can help manage - // atomic operations and coordinate multiple method calls - if (!inventoryStore.containsKey(id)) { - throw new InventoryNotFoundException(id); - } - // Multiple operations that need to be atomic - updateQuantity(id, inventory.getQuantity()); - notifyInventoryChange(id); - } -} ----- diff --git a/code/chapter11/inventory/pom.xml b/code/chapter11/inventory/pom.xml deleted file mode 100644 index dcf6eb2b..00000000 --- a/code/chapter11/inventory/pom.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - 4.0.0 - - io.microprofile - inventory - 1.0-SNAPSHOT - war - - inventory-management - https://microprofile.io - - - UTF-8 - 17 - 10.0.0 - 6.1 - 23.0.0.3 - 1.18.24 - - - 6050 - 6051 - - inventory - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - org.junit.jupiter - junit-jupiter-engine - 5.9.2 - test - - - org.junit.jupiter - junit-jupiter-api - 5.9.2 - test - - - org.mockito - mockito-core - 4.11.0 - test - - - org.mockito - mockito-junit-jupiter - 4.11.0 - test - - - - - org.glassfish.jersey.core - jersey-common - 3.1.1 - test - - - - - inventory - - - - io.openliberty.tools - liberty-maven-plugin - 3.11.2 - - inventoryServer - runnable - 120 - - /inventory - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - - org.projectlombok - lombok - ${lombok.version} - - - - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java deleted file mode 100644 index e3c98814..00000000 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.microprofile.tutorial.store.inventory; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; -import org.eclipse.microprofile.openapi.annotations.info.Contact; -import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.info.License; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * JAX-RS application for inventory management. - */ -@ApplicationPath("/api") -@OpenAPIDefinition( - info = @Info( - title = "Inventory API", - version = "1.0.0", - description = "API for managing product inventory", - license = @License( - name = "Eclipse Public License 2.0", - url = "https://www.eclipse.org/legal/epl-2.0/"), - contact = @Contact( - name = "Inventory API Support", - email = "support@example.com")), - tags = { - @Tag(name = "Inventory", description = "Operations related to product inventory management") - } -) -public class InventoryApplication extends Application { - // The resources will be discovered automatically -} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java deleted file mode 100644 index 6aed1f88..00000000 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java +++ /dev/null @@ -1,165 +0,0 @@ -package io.microprofile.tutorial.store.inventory.dto; - -import io.microprofile.tutorial.store.inventory.entity.Inventory; -import jakarta.json.bind.annotation.JsonbProperty; - -/** - * DTO that combines inventory data with product information from the catalog service. - */ -public class InventoryWithProductInfo { - - @JsonbProperty("inventory") - private Inventory inventory; - - @JsonbProperty("product") - private Product product; - - /** - * Default constructor for JSON-B. - */ - public InventoryWithProductInfo() { - } - - /** - * Constructor with parameters. - * - * @param inventory The inventory information - * @param product The product information from catalog service - */ - public InventoryWithProductInfo(Inventory inventory, Product product) { - this.inventory = inventory; - this.product = product; - } - - /** - * Gets the inventory information. - * - * @return The inventory - */ - public Inventory getInventory() { - return inventory; - } - - /** - * Sets the inventory information. - * - * @param inventory The inventory to set - */ - public void setInventory(Inventory inventory) { - this.inventory = inventory; - } - - /** - * Gets the product information. - * - * @return The product - */ - public Product getProduct() { - return product; - } - - /** - * Sets the product information. - * - * @param product The product to set - */ - public void setProduct(Product product) { - this.product = product; - } - - /** - * Gets the inventory ID. - * - * @return The inventory ID - */ - @JsonbProperty("inventoryId") - public Long getInventoryId() { - return inventory != null ? inventory.getInventoryId() : null; - } - - /** - * Gets the product ID. - * - * @return The product ID - */ - @JsonbProperty("productId") - public Long getProductId() { - return inventory != null ? inventory.getProductId() : null; - } - - /** - * Gets the product name. - * - * @return The product name - */ - @JsonbProperty("productName") - public String getProductName() { - return product != null ? product.getName() : null; - } - - /** - * Gets the product price. - * - * @return The product price - */ - @JsonbProperty("productPrice") - public Double getProductPrice() { - return product != null ? product.getPrice() : null; - } - - /** - * Gets the product category. - * - * @return The product category - */ - @JsonbProperty("productCategory") - public String getProductCategory() { - return product != null ? product.getCategory() : null; - } - - /** - * Gets the inventory quantity. - * - * @return The quantity - */ - @JsonbProperty("quantity") - public Integer getQuantity() { - return inventory != null ? inventory.getQuantity() : null; - } - - /** - * Gets the reserved quantity. - * - * @return The reserved quantity - */ - @JsonbProperty("reservedQuantity") - public Integer getReservedQuantity() { - return inventory != null ? inventory.getReservedQuantity() : null; - } - - /** - * Gets the available quantity (quantity - reserved). - * - * @return The available quantity - */ - @JsonbProperty("availableQuantity") - public Integer getAvailableQuantity() { - if (inventory == null) { - return null; - } - return inventory.getQuantity() - inventory.getReservedQuantity(); - } - - @Override - public String toString() { - return "InventoryWithProductInfo{" + - "inventoryId=" + getInventoryId() + - ", productId=" + getProductId() + - ", productName='" + getProductName() + '\'' + - ", quantity=" + getQuantity() + - ", availableQuantity=" + getAvailableQuantity() + - ", price=" + getProductPrice() + - ", category='" + getProductCategory() + '\'' + - '}'; - } -} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java deleted file mode 100644 index 0e7c8618..00000000 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.microprofile.tutorial.store.inventory.dto; - -import jakarta.json.bind.annotation.JsonbCreator; -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.json.bind.annotation.JsonbTransient; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -/** - * Product DTO for the inventory service. - * This class represents product information received from the product service. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Product { - - /** - * Unique identifier for the product. - */ - private Long id; - - /** - * Name of the product. - */ - private String name; - - /** - * Price of the product. - */ - private Double price; - - /** - * Category of the product. - */ - private String category; - - /** - * Description of the product. - */ - private String description; - - /** - * Availability status of the product. - */ - @JsonbTransient - private boolean isAvailable = true; - - @JsonbCreator - public Product( - @JsonbProperty("id") Long id, - @JsonbProperty("name") String name, - @JsonbProperty("price") Double price, - @JsonbProperty("category") String category, - @JsonbProperty("description") String description) { - this.id = id; - this.name = name; - this.price = price; - this.category = category; - this.description = description; - } - - @Override - public String toString() { - return String.format("Product{id=%d, name='%s', price=%.2f, category='%s', isAvailable=%b}", - id, name, price, category, isAvailable); - } -} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java deleted file mode 100644 index c776c7ee..00000000 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java +++ /dev/null @@ -1,13 +0,0 @@ -/** - * This package contains the Inventory Management application for the MicroProfile tutorial store. - * - * The application demonstrates a Jakarta EE and MicroProfile-based REST service - * for managing product inventory with CRUD operations. - * - * Main Components: - * - Entity class: Contains inventory data with inventory_id, product_id, and quantity - * - Repository: Provides in-memory data storage using HashMap - * - Service: Contains business logic and validation - * - Resource: REST endpoints with OpenAPI documentation - */ -package io.microprofile.tutorial.store.inventory; diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java deleted file mode 100644 index 05de8693..00000000 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java +++ /dev/null @@ -1,168 +0,0 @@ -package io.microprofile.tutorial.store.inventory.repository; - -import io.microprofile.tutorial.store.inventory.entity.Inventory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Logger; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Thread-safe in-memory repository for Inventory objects. - * This class provides CRUD operations for Inventory entities to demonstrate MicroProfile concepts. - */ -@ApplicationScoped -public class InventoryRepository { - - private static final Logger LOGGER = Logger.getLogger(InventoryRepository.class.getName()); - - // Thread-safe map for inventory storage - private final Map inventories = new ConcurrentHashMap<>(); - - // Thread-safe ID generator - private final AtomicLong idGenerator = new AtomicLong(1); - - // Secondary index for faster lookups by productId - private final Map productToInventoryIndex = new ConcurrentHashMap<>(); - - /** - * Saves an inventory item to the repository. - * If the inventory has no ID, a new ID is assigned. - * - * @param inventory The inventory to save - * @return The saved inventory with ID assigned - */ - public Inventory save(Inventory inventory) { - // Generate ID if not provided - if (inventory.getInventoryId() == null) { - inventory.setInventoryId(idGenerator.getAndIncrement()); - } else { - // Update idGenerator if the provided ID is greater than current - long nextId = inventory.getInventoryId() + 1; - while (true) { - long currentId = idGenerator.get(); - if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { - break; - } - } - } - - LOGGER.fine("Saving inventory with ID: " + inventory.getInventoryId()); - - // Update the inventory and secondary index - inventories.put(inventory.getInventoryId(), inventory); - productToInventoryIndex.put(inventory.getProductId(), inventory.getInventoryId()); - - return inventory; - } - - /** - * Finds an inventory item by ID. - * - * @param id The inventory ID - * @return An Optional containing the inventory if found, or empty if not found - */ - public Optional findById(Long id) { - if (id == null) { - LOGGER.warning("Attempted to find inventory with null ID"); - return Optional.empty(); - } - return Optional.ofNullable(inventories.get(id)); - } - - /** - * Finds inventory by product ID. - * - * @param productId The product ID - * @return An Optional containing the inventory if found, or empty if not found - */ - public Optional findByProductId(Long productId) { - if (productId == null) { - LOGGER.warning("Attempted to find inventory with null product ID"); - return Optional.empty(); - } - - // Use the secondary index for efficient lookup - Long inventoryId = productToInventoryIndex.get(productId); - if (inventoryId != null) { - return Optional.ofNullable(inventories.get(inventoryId)); - } - - // Fall back to scanning if not found in index (ensures consistency) - return inventories.values().stream() - .filter(inventory -> productId.equals(inventory.getProductId())) - .findFirst(); - } - - /** - * Retrieves all inventory items from the repository. - * - * @return A list of all inventory items - */ - public List findAll() { - return new ArrayList<>(inventories.values()); - } - - /** - * Deletes an inventory item by ID. - * - * @param id The ID of the inventory to delete - * @return true if the inventory was deleted, false if not found - */ - public boolean deleteById(Long id) { - if (id == null) { - LOGGER.warning("Attempted to delete inventory with null ID"); - return false; - } - - Inventory removed = inventories.remove(id); - if (removed != null) { - // Also remove from the secondary index - productToInventoryIndex.remove(removed.getProductId()); - LOGGER.fine("Deleted inventory with ID: " + id); - return true; - } - - LOGGER.fine("Failed to delete inventory with ID (not found): " + id); - return false; - } - - /** - * Updates an existing inventory item. - * - * @param id The ID of the inventory to update - * @param inventory The updated inventory information - * @return An Optional containing the updated inventory, or empty if not found - */ - public Optional update(Long id, Inventory inventory) { - if (id == null || inventory == null) { - LOGGER.warning("Attempted to update inventory with null ID or null inventory"); - return Optional.empty(); - } - - if (!inventories.containsKey(id)) { - LOGGER.fine("Failed to update inventory with ID (not found): " + id); - return Optional.empty(); - } - - // Get the existing inventory to update its product index if needed - Inventory existing = inventories.get(id); - if (existing != null && !existing.getProductId().equals(inventory.getProductId())) { - // Product ID changed, update the index - productToInventoryIndex.remove(existing.getProductId()); - } - - // Set ID and update the repository - inventory.setInventoryId(id); - inventories.put(id, inventory); - productToInventoryIndex.put(inventory.getProductId(), id); - - LOGGER.fine("Updated inventory with ID: " + id); - return Optional.of(inventory); - } -} diff --git a/code/chapter11/inventory/src/main/liberty/config/server.xml b/code/chapter11/inventory/src/main/liberty/config/server.xml deleted file mode 100644 index 1bc1d1f7..00000000 --- a/code/chapter11/inventory/src/main/liberty/config/server.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 - - - - - \ No newline at end of file diff --git a/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties b/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties deleted file mode 100644 index 26358a61..00000000 --- a/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties +++ /dev/null @@ -1,5 +0,0 @@ -product-service/mp-rest/url=http://localhost:5050/catalog/api -product-service/mp-rest/scope=jakarta.inject.Singleton -product-service/mp-rest/connectTimeout=5000 -product-service/mp-rest/readTimeout=10000 -product-service/mp-rest/followRedirects=true \ No newline at end of file diff --git a/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml b/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml deleted file mode 100644 index ba21c474..00000000 --- a/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml b/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 5a812df9..00000000 --- a/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Inventory Management - - index.html - - diff --git a/code/chapter11/inventory/src/main/webapp/index.html b/code/chapter11/inventory/src/main/webapp/index.html deleted file mode 100644 index d4763767..00000000 --- a/code/chapter11/inventory/src/main/webapp/index.html +++ /dev/null @@ -1,350 +0,0 @@ - - - - - - Inventory Management Service - - - -

Inventory Management Service

-

Welcome to the Inventory Management API, a Jakarta EE and MicroProfile demo featuring comprehensive MicroProfile Rest Client integration.

- -
-

🔌 MicroProfile Rest Client Integration

-

This service demonstrates three different approaches to using MicroProfile Rest Client:

-
    -
  • CDI Injection (@RestClient) - For standard product validation
  • -
  • RestClientBuilder (5s/10s timeout) - For lightweight availability checks
  • -
  • Advanced RestClientBuilder (3s/8s timeout) - For detailed product information
  • -
-

Catalog Service Integration: http://localhost:5050/catalog/api

-
- -
-

⏱️ Timeout Configuration Details

-

Our implementation demonstrates different timeout strategies for various use cases:

- -
-
-

🔌 CDI Injection (@RestClient)

-

Configuration: Via microprofile-config.properties

-

Connect Timeout: Default (30s)

-

Read Timeout: Default (30s)

-

Use Case: Standard operations with reliable timeouts

-
- -
-

⚡ RestClientBuilder (5s/10s)

-

Configuration: Programmatic

-

Connect Timeout: 5 seconds

-

Read Timeout: 10 seconds

-

Use Case: Quick availability checks

-
- -
-

🚀 Advanced RestClientBuilder (3s/8s)

-

Configuration: Programmatic

-

Connect Timeout: 3 seconds

-

Read Timeout: 8 seconds

-

Use Case: Fast product info retrieval

-
-
- -

📊 Timeout Configuration Comparison

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Client TypeConnect TimeoutRead TimeoutConfiguration MethodEndpoints Using ItPurpose
@RestClient Injection30s (default)30s (default)microprofile-config.propertiesPOST/PUT inventories, bulk operationsReliable product validation
RestClientBuilder (Standard)5 seconds10 secondsRestClientBuilder.connectTimeout()PATCH /reserve/{quantity}Quick availability checks
RestClientBuilder (Advanced)3 seconds8 secondsRestClientBuilder.readTimeout()GET /product-info/{productId}Fast product information
- -

🔧 Timeout Configuration Code Examples

-

1. CDI Injection Configuration (microprofile-config.properties):

-
# Default timeouts - can be customized via properties
-io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/url=http://localhost:5050/catalog/api
-io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/scope=javax.inject.Singleton
-
-# Optional custom timeouts (if needed):
-# io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/connectTimeout=30000
-# io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/readTimeout=30000
- -

2. RestClientBuilder with 5s/10s Timeouts (Availability Check):

-
ProductServiceClient dynamicClient = RestClientBuilder.newBuilder()
-    .baseUri(URI.create("http://localhost:5050/catalog/api"))
-    .connectTimeout(5, TimeUnit.SECONDS)    // 5 seconds to establish connection
-    .readTimeout(10, TimeUnit.SECONDS)      // 10 seconds to read response
-    .build(ProductServiceClient.class);
- -

3. Advanced RestClientBuilder with 3s/8s Timeouts (Product Info):

-
ProductServiceClient customClient = RestClientBuilder.newBuilder()
-    .baseUri(URI.create("http://localhost:5050/catalog/api"))
-    .connectTimeout(3, TimeUnit.SECONDS)    // 3 seconds to establish connection
-    .readTimeout(8, TimeUnit.SECONDS)       // 8 seconds to read response
-    .build(ProductServiceClient.class);
- -

📈 Timeout Strategy Benefits

-
    -
  • Connect Timeout: Prevents hanging when catalog service is unreachable
  • -
  • Read Timeout: Ensures timely response even if catalog service is slow
  • -
  • Different Strategies: Optimized timeouts for different operation types
  • -
  • Fail-Fast Behavior: Quick error detection and graceful degradation
  • -
  • Performance Optimization: Shorter timeouts for non-critical operations
  • -
-
- -

Available Endpoints:

- -
-

OpenAPI Documentation API Docs

-

GET /openapi - Access OpenAPI documentation

- View API Documentation -
- -
-

Basic Inventory Operations

-

GET /api/inventories - Get all inventory items

-

GET /api/inventories/{id} - Get inventory by ID

-

GET /api/inventories/product/{productId} - Get inventory by product ID

-

POST /api/inventories - Create new inventory @RestClient Validation

-

PUT /api/inventories/{id} - Update inventory @RestClient Validation

-

DELETE /api/inventories/{id} - Delete inventory

-

PATCH /api/inventories/product/{productId}/quantity/{quantity} - Update product quantity

-
- -
-

🔌 MicroProfile Rest Client Features

- -

1. Injected REST Client (@RestClient)

-

POST /api/inventories - Product validation during inventory creation

-

PUT /api/inventories/{id} - Product validation during inventory updates

-

POST /api/inventories/bulk - Bulk inventory creation with validation

-

GET /api/inventories/{id}/with-product-info - Enriched inventory with product details

-

GET /api/inventories/category/{category} - Inventories filtered by product category

- -

2. RestClientBuilder (5s connect / 10s read timeout)

-

PATCH /api/inventories/product/{productId}/reserve/{quantity} - Reserve inventory with availability check

- -

3. Advanced RestClientBuilder (3s connect / 8s read timeout)

-

GET /api/inventories/product-info/{productId} - Get detailed product information

-
- -
-

🚀 Advanced Features

-

GET /api/inventories?page={page}&size={size} - Pagination support

-

GET /api/inventories?minQuantity={min}&maxQuantity={max} - Quantity filtering

-

GET /api/inventories/count?minQuantity={min}&maxQuantity={max} - Count with filters

-

POST /api/inventories/bulk - Bulk inventory operations

-
- -

Example Requests

- -
-

💡 Quick Start Examples

- -

1. Basic Operations

-
# Get all inventories
-curl -X GET http://localhost:7050/inventory/api/inventories
-
-# Create inventory (with automatic product validation)
-curl -X POST http://localhost:7050/inventory/api/inventories \
-  -H "Content-Type: application/json" \
-  -d '{"productId": 1, "quantity": 100, "reservedQuantity": 0}'
- -

2. MicroProfile Rest Client Features

-
# Reserve inventory (uses RestClientBuilder for availability check)
-curl -X PATCH http://localhost:7050/inventory/api/inventories/product/1/reserve/10
-
-# Get product info (uses Advanced RestClientBuilder)
-curl -X GET http://localhost:7050/inventory/api/inventories/product-info/1
-
-# Get enriched inventory with product details
-curl -X GET http://localhost:7050/inventory/api/inventories/1/with-product-info
- -

3. Advanced Features

-
# Pagination and filtering
-curl -X GET "http://localhost:7050/inventory/api/inventories?page=0&size=5&minQuantity=50"
-
-# Bulk operations
-curl -X POST http://localhost:7050/inventory/api/inventories/bulk \
-  -H "Content-Type: application/json" \
-  -d '[{"productId": 1, "quantity": 100}, {"productId": 2, "quantity": 50}]'
-
- -
-

⚙️ Configuration & Testing

-

Test Scripts Available:

-
    -
  • ./test-inventory-endpoints.sh - Comprehensive test suite
  • -
  • ./test-inventory-endpoints.sh --restclient - RestClient features only
  • -
  • ./quick-test-commands.sh - Command reference
  • -
- -

Service Dependencies:

-
    -
  • Catalog Service: http://localhost:5050
  • -
  • Inventory Service: http://localhost:7050
  • -
- -

MicroProfile Config:

-
io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/url=http://localhost:5050/catalog/api
-io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/scope=javax.inject.Singleton
-
- -

🏗️ Architecture

-
-

MicroProfile Rest Client Integration Patterns

-

Pattern 1 - CDI Injection: Automatic client injection with configuration-driven setup

-

Pattern 2 - Programmatic Creation: Dynamic client building with custom timeouts and error handling

-

Pattern 3 - Advanced Configuration: Per-use-case client optimization

- -

Technologies Used:

-
    -
  • Jakarta EE 10
  • -
  • MicroProfile 6.1 (Rest Client, OpenAPI, Config)
  • -
  • Open Liberty 24.0.0.x
  • -
  • Jackson for JSON processing
  • -
  • Lombok for reduced boilerplate
  • -
-
- -
-

MicroProfile REST Client Tutorial - Inventory Service

-

Demonstrates comprehensive MicroProfile Rest Client integration with Jakarta EE

-

© 2025 - Updated June 7, 2025

-
- - diff --git a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java deleted file mode 100644 index a4349eb7..00000000 --- a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package io.microprofile.tutorial.store.inventory.integration; - -import io.microprofile.tutorial.store.inventory.service.InventoryService; -import io.microprofile.tutorial.store.inventory.entity.Inventory; -import io.microprofile.tutorial.store.inventory.dto.Product; -import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; -import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; - -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 java.lang.reflect.Field; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Integration tests for InventoryService with ProductServiceClient. - * This test class focuses on the main integration points. - */ -@ExtendWith(MockitoExtension.class) -class InventoryServiceIntegrationTest { - - @Mock - private InventoryRepository inventoryRepository; - - @Mock - private ProductServiceClient productServiceClient; - - @InjectMocks - private InventoryService inventoryService; - - private Product mockProduct; - private Inventory mockInventory; - - @BeforeEach - void setUp() throws Exception { - // Create mock product using constructor - mockProduct = new Product(1L, "Test Product", 29.99, "Electronics", "A test product"); - - // Create mock inventory with proper productId set using reflection - mockInventory = new Inventory(); - setPrivateField(mockInventory, "inventoryId", 1L); - setPrivateField(mockInventory, "productId", 1L); - setPrivateField(mockInventory, "quantity", 10); - setPrivateField(mockInventory, "reservedQuantity", 2); - } - - private void setPrivateField(Object obj, String fieldName, Object value) throws Exception { - Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(obj, value); - } - - private Object getPrivateField(Object obj, String fieldName) throws Exception { - Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(obj); - } - - @Test - void testProductServiceClientIntegration_BasicCall() throws Exception { - // Arrange - lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); - - // Act - Product result = inventoryService.getProductInfo(mockInventory); - - // Assert - assertNotNull(result); - assertEquals(1L, getPrivateField(result, "id")); - verify(productServiceClient).getProductById(1L); - } - - @Test - void testCreateInventory_CallsProductValidation() throws Exception { - // Arrange - Inventory newInventory = new Inventory(); - setPrivateField(newInventory, "productId", 1L); - setPrivateField(newInventory, "quantity", 5); - setPrivateField(newInventory, "reservedQuantity", 0); - - lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); - lenient().when(inventoryRepository.findByProductId(anyLong())).thenReturn(Optional.empty()); - lenient().when(inventoryRepository.save(any(Inventory.class))).thenReturn(mockInventory); - - // Act - Inventory result = inventoryService.createInventory(newInventory); - - // Assert - assertNotNull(result); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).save(newInventory); - } - - @Test - void testUpdateInventory_CallsProductValidation() throws Exception { - // Arrange - Inventory updatedInventory = new Inventory(); - setPrivateField(updatedInventory, "productId", 1L); - setPrivateField(updatedInventory, "quantity", 15); - - lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); - lenient().when(inventoryRepository.findByProductId(anyLong())).thenReturn(Optional.of(mockInventory)); - lenient().when(inventoryRepository.update(anyLong(), any(Inventory.class))).thenReturn(Optional.of(mockInventory)); - - // Act - Inventory result = inventoryService.updateInventory(1L, updatedInventory); - - // Assert - assertNotNull(result); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).update(1L, updatedInventory); - } - - @Test - void testProductServiceClient_ReturnsProductData() throws Exception { - // Arrange - lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); - - // Act - Product result = inventoryService.getProductInfo(mockInventory); - - // Assert - assertNotNull(result); - // Test using reflection to access private fields - assertEquals("Test Product", getPrivateField(result, "name")); - assertEquals("Electronics", getPrivateField(result, "category")); - verify(productServiceClient).getProductById(1L); - } - - @Test - void testCreateInventory_WithInvalidProduct_ThrowsException() throws Exception { - // Arrange - Inventory newInventory = new Inventory(); - setPrivateField(newInventory, "productId", 999L); - setPrivateField(newInventory, "quantity", 5); - - lenient().when(productServiceClient.getProductById(999L)).thenReturn(null); - - // Act & Assert - RuntimeException exception = assertThrows(RuntimeException.class, () -> { - inventoryService.createInventory(newInventory); - }); - - assertTrue(exception.getMessage().contains("Product not found in catalog with ID: 999")); - verify(productServiceClient).getProductById(999L); - } -} \ No newline at end of file diff --git a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java deleted file mode 100644 index a7859a74..00000000 --- a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package io.microprofile.tutorial.store.inventory.service; - -import io.microprofile.tutorial.store.inventory.entity.Inventory; -import io.microprofile.tutorial.store.inventory.exception.InventoryNotFoundException; -import io.microprofile.tutorial.store.inventory.exception.InventoryConflictException; -import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; -import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; -import io.microprofile.tutorial.store.inventory.dto.Product; -import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; - -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 jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Unit tests for InventoryService with ProductServiceClient integration. - */ -@ExtendWith(MockitoExtension.class) -class InventoryServiceTest { - - @Mock - private InventoryRepository inventoryRepository; - - @Mock - private ProductServiceClient productServiceClient; - - @InjectMocks - private InventoryService inventoryService; - - private Product mockProduct; - private Inventory mockInventory; - - @BeforeEach - void setUp() { - mockProduct = new Product(1L, "Test Product", 29.99, "Electronics", "A test product"); - - mockInventory = new Inventory(); - mockInventory.setInventoryId(1L); - mockInventory.setProductId(1L); - mockInventory.setQuantity(100); - mockInventory.setReservedQuantity(10); - } - - @Test - void testCreateInventory_WithValidProduct_ShouldSucceed() { - // Arrange - Inventory newInventory = Inventory.builder() - .productId(1L) - .quantity(50) - .reservedQuantity(0) - .build(); - - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.empty()); - when(inventoryRepository.save(any(Inventory.class))).thenReturn(mockInventory); - - // Act - Inventory result = inventoryService.createInventory(newInventory); - - // Assert - assertNotNull(result); - assertEquals(1L, result.getInventoryId()); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).findByProductId(1L); - verify(inventoryRepository).save(newInventory); - } - - @Test - void testCreateInventory_WithInvalidProduct_ShouldThrowNotFoundException() { - // Arrange - Inventory newInventory = Inventory.builder() - .productId(999L) - .quantity(50) - .reservedQuantity(0) - .build(); - - when(productServiceClient.getProductById(999L)).thenReturn(null); - - // Act & Assert - InventoryNotFoundException exception = assertThrows( - InventoryNotFoundException.class, - () -> inventoryService.createInventory(newInventory) - ); - - assertTrue(exception.getMessage().contains("Product not found in catalog with ID: 999")); - verify(productServiceClient).getProductById(999L); - verify(inventoryRepository, never()).save(any()); - } - - @Test - void testCreateInventory_WithExistingInventory_ShouldThrowConflictException() { - // Arrange - Inventory newInventory = Inventory.builder() - .productId(1L) - .quantity(50) - .reservedQuantity(0) - .build(); - - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); - - // Act & Assert - InventoryConflictException exception = assertThrows( - InventoryConflictException.class, - () -> inventoryService.createInventory(newInventory) - ); - - assertTrue(exception.getMessage().contains("Inventory for product already exists")); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).findByProductId(1L); - verify(inventoryRepository, never()).save(any()); - } - - @Test - void testUpdateInventory_WithValidProduct_ShouldSucceed() { - // Arrange - Inventory updatedInventory = Inventory.builder() - .productId(1L) - .quantity(75) - .reservedQuantity(5) - .build(); - - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); - when(inventoryRepository.update(1L, updatedInventory)).thenReturn(Optional.of(mockInventory)); - - // Act - Inventory result = inventoryService.updateInventory(1L, updatedInventory); - - // Assert - assertNotNull(result); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).findByProductId(1L); - verify(inventoryRepository).update(1L, updatedInventory); - } - - @Test - void testUpdateInventory_WithProductConflict_ShouldThrowConflictException() { - // Arrange - Inventory existingInventory = Inventory.builder() - .inventoryId(2L) - .productId(1L) - .quantity(25) - .reservedQuantity(0) - .build(); - - Inventory updatedInventory = Inventory.builder() - .productId(1L) - .quantity(75) - .reservedQuantity(5) - .build(); - - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(existingInventory)); - - // Act & Assert - InventoryConflictException exception = assertThrows( - InventoryConflictException.class, - () -> inventoryService.updateInventory(1L, updatedInventory) - ); - - assertTrue(exception.getMessage().contains("Another inventory record already exists")); - verify(productServiceClient).getProductById(1L); - verify(inventoryRepository).findByProductId(1L); - verify(inventoryRepository, never()).update(anyLong(), any()); - } - - @Test - void testCreateBulkInventories_WithValidProducts_ShouldSucceed() { - // Arrange - Product product2 = new Product(2L, "Product 2", 19.99, "Home", "Another product"); - - Inventory inventory1 = Inventory.builder().productId(1L).quantity(50).reservedQuantity(0).build(); - Inventory inventory2 = Inventory.builder().productId(2L).quantity(25).reservedQuantity(0).build(); - List inventories = Arrays.asList(inventory1, inventory2); - - Inventory saved1 = Inventory.builder().inventoryId(1L).productId(1L).quantity(50).reservedQuantity(0).build(); - Inventory saved2 = Inventory.builder().inventoryId(2L).productId(2L).quantity(25).reservedQuantity(0).build(); - - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - when(productServiceClient.getProductById(2L)).thenReturn(product2); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.empty()); - when(inventoryRepository.findByProductId(2L)).thenReturn(Optional.empty()); - when(inventoryRepository.save(inventory1)).thenReturn(saved1); - when(inventoryRepository.save(inventory2)).thenReturn(saved2); - - // Act - List result = inventoryService.createBulkInventories(inventories); - - // Assert - assertNotNull(result); - assertEquals(2, result.size()); - verify(productServiceClient).getProductById(1L); - verify(productServiceClient).getProductById(2L); - verify(inventoryRepository, times(2)).save(any(Inventory.class)); - } - - @Test - void testGetInventoryWithProductInfo_ShouldReturnEnrichedData() { - // Arrange - when(inventoryRepository.findById(1L)).thenReturn(Optional.of(mockInventory)); - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - - // Act - InventoryWithProductInfo result = inventoryService.getInventoryWithProductInfo(1L); - - // Assert - assertNotNull(result); - assertEquals(mockInventory, result.getInventory()); - assertEquals(mockProduct, result.getProduct()); - assertEquals("Test Product", result.getProductName()); - assertEquals(29.99, result.getProductPrice()); - assertEquals("Electronics", result.getProductCategory()); - assertEquals(100, result.getQuantity()); - assertEquals(10, result.getReservedQuantity()); - assertEquals(90, result.getAvailableQuantity()); - } - - @Test - void testGetInventoriesByCategory_ShouldReturnFilteredInventories() { - // Arrange - Product product2 = new Product(2L, "Product 2", 39.99, "Electronics", "Another electronics product"); - List electronicsProducts = Arrays.asList(mockProduct, product2); - - Inventory inventory2 = Inventory.builder() - .inventoryId(2L) - .productId(2L) - .quantity(75) - .reservedQuantity(5) - .build(); - - when(productServiceClient.getProductsByCategory("Electronics")).thenReturn(electronicsProducts); - when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); - when(inventoryRepository.findByProductId(2L)).thenReturn(Optional.of(inventory2)); - - // Act - List result = inventoryService.getInventoriesByCategory("Electronics"); - - // Assert - assertNotNull(result); - assertEquals(2, result.size()); - - InventoryWithProductInfo first = result.get(0); - assertEquals("Test Product", first.getProductName()); - assertEquals("Electronics", first.getProductCategory()); - - InventoryWithProductInfo second = result.get(1); - assertEquals("Product 2", second.getProductName()); - assertEquals("Electronics", second.getProductCategory()); - - verify(productServiceClient).getProductsByCategory("Electronics"); - verify(inventoryRepository).findByProductId(1L); - verify(inventoryRepository).findByProductId(2L); - } - - @Test - void testGetProductInfo_ShouldReturnProductDetails() { - // Arrange - when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); - - // Act - Product result = inventoryService.getProductInfo(mockInventory); - - // Assert - assertNotNull(result); - assertEquals("Test Product", result.getName()); - assertEquals(29.99, result.getPrice()); - assertEquals("Electronics", result.getCategory()); - verify(productServiceClient).getProductById(1L); - } - - @Test - void testValidateProductExists_WithServiceError_ShouldThrowRuntimeException() { - // Arrange - WebApplicationException serviceException = new WebApplicationException( - Response.status(500).build() - ); - when(productServiceClient.getProductById(1L)).thenThrow(serviceException); - - // Act & Assert - RuntimeException exception = assertThrows( - RuntimeException.class, - () -> inventoryService.createInventory(mockInventory) - ); - - assertTrue(exception.getMessage().contains("Failed to validate product with catalog service")); - verify(productServiceClient).getProductById(1L); - } -} diff --git a/code/chapter11/order/Dockerfile b/code/chapter11/order/Dockerfile deleted file mode 100644 index 68549649..00000000 --- a/code/chapter11/order/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM openliberty/open-liberty:23.0.0.3-full-java17-openj9-ubi - -# Copy Liberty configuration -COPY --chown=1001:0 src/main/liberty/config/ /config/ - -# Copy application WAR file -COPY --chown=1001:0 target/order.war /config/apps/ - -# Set environment variables -ENV PORT=9080 - -# Configure the server to run -RUN configure.sh - -# Expose ports -EXPOSE 8050 8051 - -# Start the server -CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/order/README.md b/code/chapter11/order/README.md deleted file mode 100644 index e8202c7c..00000000 --- a/code/chapter11/order/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Order Service - -A Jakarta EE and MicroProfile-based REST service for order management in the Liberty Rest App demo. - -## Features - -- Provides CRUD operations for order management -- Tracks orders with order_id, user_id, total_price, and status -- Manages order items with order_item_id, order_id, product_id, quantity, and price_at_order -- Uses Jakarta EE 10.0 and MicroProfile 6.1 -- Runs on Open Liberty runtime - -## Running the Application - -There are multiple ways to run the application: - -### Using Maven - -``` -cd order -mvn liberty:run -``` - -### Using the provided script - -``` -./run.sh -``` - -### Using Docker - -``` -./run-docker.sh -``` - -This will start the Open Liberty server on port 8050 (HTTP) and 8051 (HTTPS). - -## API Endpoints - -| Method | URL | Description | -|--------|:----------------------------------------|:-------------------------------------| -| GET | /api/orders | Get all orders | -| GET | /api/orders/{id} | Get order by ID | -| GET | /api/orders/user/{userId} | Get orders by user ID | -| GET | /api/orders/status/{status} | Get orders by status | -| POST | /api/orders | Create new order | -| PUT | /api/orders/{id} | Update order | -| DELETE | /api/orders/{id} | Delete order | -| PATCH | /api/orders/{id}/status/{status} | Update order status | -| GET | /api/orders/{orderId}/items | Get items for an order | -| GET | /api/orders/items/{orderItemId} | Get specific order item | -| POST | /api/orders/{orderId}/items | Add item to order | -| PUT | /api/orders/items/{orderItemId} | Update order item | -| DELETE | /api/orders/items/{orderItemId} | Delete order item | - -## Testing with cURL - -### Create new order -``` -curl -X POST http://localhost:8050/order/api/orders \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "CREATED", - "orderItems": [ - { - "productId": 101, - "quantity": 2, - "priceAtOrder": 49.99 - }, - { - "productId": 102, - "quantity": 1, - "priceAtOrder": 50.00 - } - ] - }' -``` - -### Get all orders -``` -curl -X GET http://localhost:8050/order/api/orders -``` - -### Get order by ID -``` -curl -X GET http://localhost:8050/order/api/orders/1 -``` - -### Get orders by user ID -``` -curl -X GET http://localhost:8050/order/api/orders/user/1 -``` - -### Update order -``` -curl -X PUT http://localhost:8050/order/api/orders/1 \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "PAID" - }' -``` -### Get items for an order -``` -curl -X GET http://localhost:8050/order/api/orders/1/items -``` - -### Add item to order -``` -curl -X POST http://localhost:8050/order/api/orders/1/items \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 103, - "quantity": 1, - "priceAtOrder": 29.99 - }' -``` - -### Update order item -``` -curl -X PUT http://localhost:8050/order/api/orders/items/1 \ - -H "Content-Type: application/json" \ - -d '{ - "orderId": 1, - "productId": 103, - "quantity": 2, - "priceAtOrder": 29.99 - }' -``` - -### Update order status -``` -curl -X PATCH http://localhost:8050/order/api/orders/1/status/SHIPPED -``` - -### Delete order item -``` -curl -X DELETE http://localhost:8050/order/api/orders/items/1 -``` - -### Delete order -``` -curl -X DELETE http://localhost:8050/order/api/orders/1 -``` diff --git a/code/chapter11/order/pom.xml b/code/chapter11/order/pom.xml deleted file mode 100644 index ff7fdc97..00000000 --- a/code/chapter11/order/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - io.microprofile - order - 1.0-SNAPSHOT - war - - order-management - https://microprofile.io - - - UTF-8 - 17 - 10.0.0 - 6.1 - 23.0.0.3 - 1.18.24 - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - order - - - - io.openliberty.tools - liberty-maven-plugin - 3.8.2 - - orderServer - runnable - 120 - - /order - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/order/restart-server.sh b/code/chapter11/order/restart-server.sh deleted file mode 100755 index cf673ccb..00000000 --- a/code/chapter11/order/restart-server.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# Script to restart the Liberty server and test the Swagger UI - -# Change to the order directory -cd /workspaces/liberty-rest-app/order - -# Build the application -echo "Building the Order Service application..." -mvn clean package - -# Stop the Liberty server -echo "Stopping Liberty server..." -mvn liberty:stop - -# Copy the public key to the Liberty server config directory -echo "Copying public key to Liberty config directory..." -mkdir -p target/liberty/wlp/usr/servers/orderServer -cp src/main/resources/META-INF/publicKey.pem target/liberty/wlp/usr/servers/orderServer/ - -# Start the Liberty server -echo "Starting Liberty server..." -mvn liberty:start - -# Wait for the server to start -echo "Waiting for server to start..." -sleep 10 - -# Print URLs for testing -echo "" -echo "Server started. You can access the following URLs:" -echo "- API Documentation: http://localhost:8050/order/openapi/ui" -echo "- Custom Swagger UI: http://localhost:8050/order/swagger.html" -echo "- Home Page: http://localhost:8050/order/index.html" -echo "" -echo "If Swagger UI has CORS issues, use the custom Swagger UI at /swagger.html" diff --git a/code/chapter11/order/run-docker.sh b/code/chapter11/order/run-docker.sh deleted file mode 100755 index c3d89128..00000000 --- a/code/chapter11/order/run-docker.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Build the application -mvn clean package - -# Build the Docker image -docker build -t order-service . - -# Run the container -docker run -d --name order-service -p 8050:8050 -p 8051:8051 order-service diff --git a/code/chapter11/order/run.sh b/code/chapter11/order/run.sh deleted file mode 100755 index 7b7db548..00000000 --- a/code/chapter11/order/run.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Navigate to the order service directory -cd "$(dirname "$0")" - -# Build the project -echo "Building Order Service..." -mvn clean package - -# Run the Liberty server -echo "Starting Order Service..." -mvn liberty:run diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java deleted file mode 100644 index 3113aac8..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.microprofile.tutorial.store.order; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; -import org.eclipse.microprofile.openapi.annotations.info.Contact; -import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.info.License; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * JAX-RS application for order management. - */ -@ApplicationPath("/api") -@OpenAPIDefinition( - info = @Info( - title = "Order API", - version = "1.0.0", - description = "API for managing orders and order items", - license = @License( - name = "Eclipse Public License 2.0", - url = "https://www.eclipse.org/legal/epl-2.0/"), - contact = @Contact( - name = "Order API Support", - email = "support@example.com")), - tags = { - @Tag(name = "Order", description = "Operations related to order management"), - @Tag(name = "OrderItem", description = "Operations related to order item management") - } -) -public class OrderApplication extends Application { - // The resources will be discovered automatically -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java deleted file mode 100644 index c1d8be1c..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.microprofile.tutorial.store.order.entity; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * Order class for the microprofile tutorial store application. - * This class represents an order in the system with its details. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Order { - - private Long orderId; - - @NotNull(message = "User ID cannot be null") - private Long userId; - - @NotNull(message = "Total price cannot be null") - @Min(value = 0, message = "Total price must be greater than or equal to 0") - private BigDecimal totalPrice; - - @NotNull(message = "Status cannot be null") - private OrderStatus status; - - private LocalDateTime createdAt; - - private LocalDateTime updatedAt; - - @Builder.Default - private List orderItems = new ArrayList<>(); -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java deleted file mode 100644 index ef849969..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.microprofile.tutorial.store.order.entity; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -/** - * OrderItem class for the microprofile tutorial store application. - * This class represents an item within an order. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class OrderItem { - - private Long orderItemId; - - @NotNull(message = "Order ID cannot be null") - private Long orderId; - - @NotNull(message = "Product ID cannot be null") - private Long productId; - - @NotNull(message = "Quantity cannot be null") - @Min(value = 1, message = "Quantity must be at least 1") - private Integer quantity; - - @NotNull(message = "Price at order cannot be null") - @Min(value = 0, message = "Price must be greater than or equal to 0") - private BigDecimal priceAtOrder; -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java deleted file mode 100644 index af04ec26..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.microprofile.tutorial.store.order.entity; - -/** - * OrderStatus enum for the microprofile tutorial store application. - * This enum defines the possible statuses for an order. - */ -public enum OrderStatus { - CREATED, - PAID, - PROCESSING, - SHIPPED, - DELIVERED, - CANCELLED -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java deleted file mode 100644 index 9c72ad80..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * This package contains the Order Management application for the MicroProfile tutorial store. - * - * The application demonstrates a Jakarta EE and MicroProfile-based REST service - * for managing orders and order items with CRUD operations. - * - * Main Components: - * - Entity classes: Contains order data with order_id, user_id, total_price, status - * and order item data with order_item_id, order_id, product_id, quantity, price_at_order - * - Repository: Provides in-memory data storage using HashMap - * - Service: Contains business logic and validation - * - Resource: REST endpoints with OpenAPI documentation - */ -package io.microprofile.tutorial.store.order; diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java deleted file mode 100644 index 1aa11cf6..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.microprofile.tutorial.store.order.repository; - -import io.microprofile.tutorial.store.order.entity.OrderItem; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Simple in-memory repository for OrderItem objects. - * This class provides CRUD operations for OrderItem entities to demonstrate MicroProfile concepts. - */ -@ApplicationScoped -public class OrderItemRepository { - - private final Map orderItems = new HashMap<>(); - private long nextId = 1; - - /** - * Saves an order item to the repository. - * If the order item has no ID, a new ID is assigned. - * - * @param orderItem The order item to save - * @return The saved order item with ID assigned - */ - public OrderItem save(OrderItem orderItem) { - if (orderItem.getOrderItemId() == null) { - orderItem.setOrderItemId(nextId++); - } - orderItems.put(orderItem.getOrderItemId(), orderItem); - return orderItem; - } - - /** - * Finds an order item by ID. - * - * @param id The order item ID - * @return An Optional containing the order item if found, or empty if not found - */ - public Optional findById(Long id) { - return Optional.ofNullable(orderItems.get(id)); - } - - /** - * Finds order items by order ID. - * - * @param orderId The order ID - * @return A list of order items for the specified order - */ - public List findByOrderId(Long orderId) { - return orderItems.values().stream() - .filter(item -> item.getOrderId().equals(orderId)) - .collect(Collectors.toList()); - } - - /** - * Finds order items by product ID. - * - * @param productId The product ID - * @return A list of order items for the specified product - */ - public List findByProductId(Long productId) { - return orderItems.values().stream() - .filter(item -> item.getProductId().equals(productId)) - .collect(Collectors.toList()); - } - - /** - * Retrieves all order items from the repository. - * - * @return A list of all order items - */ - public List findAll() { - return new ArrayList<>(orderItems.values()); - } - - /** - * Deletes an order item by ID. - * - * @param id The ID of the order item to delete - * @return true if the order item was deleted, false if not found - */ - public boolean deleteById(Long id) { - return orderItems.remove(id) != null; - } - - /** - * Deletes all order items for an order. - * - * @param orderId The ID of the order - * @return The number of order items deleted - */ - public int deleteByOrderId(Long orderId) { - List itemsToDelete = orderItems.values().stream() - .filter(item -> item.getOrderId().equals(orderId)) - .map(OrderItem::getOrderItemId) - .collect(Collectors.toList()); - - itemsToDelete.forEach(orderItems::remove); - return itemsToDelete.size(); - } - - /** - * Updates an existing order item. - * - * @param id The ID of the order item to update - * @param orderItem The updated order item information - * @return An Optional containing the updated order item, or empty if not found - */ - public Optional update(Long id, OrderItem orderItem) { - if (!orderItems.containsKey(id)) { - return Optional.empty(); - } - - orderItem.setOrderItemId(id); - orderItems.put(id, orderItem); - return Optional.of(orderItem); - } -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java deleted file mode 100644 index 743bd26d..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.microprofile.tutorial.store.order.repository; - -import io.microprofile.tutorial.store.order.entity.Order; -import io.microprofile.tutorial.store.order.entity.OrderStatus; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Simple in-memory repository for Order objects. - * This class provides CRUD operations for Order entities to demonstrate MicroProfile concepts. - */ -@ApplicationScoped -public class OrderRepository { - - private final Map orders = new HashMap<>(); - private long nextId = 1; - - /** - * Saves an order to the repository. - * If the order has no ID, a new ID is assigned. - * - * @param order The order to save - * @return The saved order with ID assigned - */ - public Order save(Order order) { - if (order.getOrderId() == null) { - order.setOrderId(nextId++); - } - orders.put(order.getOrderId(), order); - return order; - } - - /** - * Finds an order by ID. - * - * @param id The order ID - * @return An Optional containing the order if found, or empty if not found - */ - public Optional findById(Long id) { - return Optional.ofNullable(orders.get(id)); - } - - /** - * Finds orders by user ID. - * - * @param userId The user ID - * @return A list of orders for the specified user - */ - public List findByUserId(Long userId) { - return orders.values().stream() - .filter(order -> order.getUserId().equals(userId)) - .collect(Collectors.toList()); - } - - /** - * Finds orders by status. - * - * @param status The order status - * @return A list of orders with the specified status - */ - public List findByStatus(OrderStatus status) { - return orders.values().stream() - .filter(order -> order.getStatus().equals(status)) - .collect(Collectors.toList()); - } - - /** - * Retrieves all orders from the repository. - * - * @return A list of all orders - */ - public List findAll() { - return new ArrayList<>(orders.values()); - } - - /** - * Deletes an order by ID. - * - * @param id The ID of the order to delete - * @return true if the order was deleted, false if not found - */ - public boolean deleteById(Long id) { - return orders.remove(id) != null; - } - - /** - * Updates an existing order. - * - * @param id The ID of the order to update - * @param order The updated order information - * @return An Optional containing the updated order, or empty if not found - */ - public Optional update(Long id, Order order) { - if (!orders.containsKey(id)) { - return Optional.empty(); - } - - order.setOrderId(id); - orders.put(id, order); - return Optional.of(order); - } -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java deleted file mode 100644 index e20d36f5..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java +++ /dev/null @@ -1,149 +0,0 @@ -package io.microprofile.tutorial.store.order.resource; - -import io.microprofile.tutorial.store.order.entity.OrderItem; -import io.microprofile.tutorial.store.order.service.OrderService; - -import java.net.URI; -import java.util.List; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * REST resource for order item operations. - */ -@Path("/orderItems") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "OrderItem", description = "Operations related to order item management") -public class OrderItemResource { - - @Inject - private OrderService orderService; - - @Context - private UriInfo uriInfo; - - @GET - @Path("/{id}") - @Operation(summary = "Get order item by ID", description = "Returns a specific order item by ID") - @APIResponse( - responseCode = "200", - description = "Order item", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = OrderItem.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order item not found" - ) - public OrderItem getOrderItemById( - @Parameter(description = "ID of the order item", required = true) - @PathParam("id") Long id) { - return orderService.getOrderItemById(id); - } - - @GET - @Path("/order/{orderId}") - @Operation(summary = "Get order items by order ID", description = "Returns items for a specific order") - @APIResponse( - responseCode = "200", - description = "List of order items", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = OrderItem.class) - ) - ) - public List getOrderItemsByOrderId( - @Parameter(description = "Order ID", required = true) - @PathParam("orderId") Long orderId) { - return orderService.getOrderItemsByOrderId(orderId); - } - - @POST - @Path("/order/{orderId}") - @Operation(summary = "Add item to order", description = "Adds a new item to an existing order") - @APIResponse( - responseCode = "201", - description = "Order item added", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = OrderItem.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order not found" - ) - public Response addOrderItem( - @Parameter(description = "ID of the order", required = true) - @PathParam("orderId") Long orderId, - @Parameter(description = "Order item details", required = true) - @NotNull @Valid OrderItem orderItem) { - OrderItem createdItem = orderService.addOrderItem(orderId, orderItem); - URI location = uriInfo.getBaseUriBuilder() - .path(OrderItemResource.class) - .path(createdItem.getOrderItemId().toString()) - .build(); - return Response.created(location).entity(createdItem).build(); - } - - @PUT - @Path("/{id}") - @Operation(summary = "Update order item", description = "Updates an existing order item") - @APIResponse( - responseCode = "200", - description = "Order item updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = OrderItem.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order item not found" - ) - public OrderItem updateOrderItem( - @Parameter(description = "ID of the order item", required = true) - @PathParam("id") Long id, - @Parameter(description = "Updated order item details", required = true) - @NotNull @Valid OrderItem orderItem) { - return orderService.updateOrderItem(id, orderItem); - } - - @DELETE - @Path("/{id}") - @Operation(summary = "Delete order item", description = "Deletes an order item") - @APIResponse( - responseCode = "204", - description = "Order item deleted" - ) - @APIResponse( - responseCode = "404", - description = "Order item not found" - ) - public Response deleteOrderItem( - @Parameter(description = "ID of the order item", required = true) - @PathParam("id") Long id) { - orderService.deleteOrderItem(id); - return Response.noContent().build(); - } -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java deleted file mode 100644 index 955b0442..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java +++ /dev/null @@ -1,208 +0,0 @@ -package io.microprofile.tutorial.store.order.resource; - -import io.microprofile.tutorial.store.order.entity.Order; -import io.microprofile.tutorial.store.order.entity.OrderStatus; -import io.microprofile.tutorial.store.order.service.OrderService; - -import java.net.URI; -import java.util.List; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * REST resource for order operations. - */ -@Path("/orders") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Order", description = "Operations related to order management") -public class OrderResource { - - @Inject - private OrderService orderService; - - @Context - private UriInfo uriInfo; - - @GET - @Operation(summary = "Get all orders", description = "Returns a list of all orders with their items") - @APIResponse( - responseCode = "200", - description = "List of orders", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) - ) - ) - public List getAllOrders() { - return orderService.getAllOrders(); - } - - @GET - @Path("/{id}") - @Operation(summary = "Get order by ID", description = "Returns a specific order by ID with its items") - @APIResponse( - responseCode = "200", - description = "Order", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Order.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order not found" - ) - public Order getOrderById( - @Parameter(description = "ID of the order", required = true) - @PathParam("id") Long id) { - return orderService.getOrderById(id); - } - - @GET - @Path("/user/{userId}") - @Operation(summary = "Get orders by user ID", description = "Returns orders for a specific user") - @APIResponse( - responseCode = "200", - description = "List of orders", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) - ) - ) - public List getOrdersByUserId( - @Parameter(description = "User ID", required = true) - @PathParam("userId") Long userId) { - return orderService.getOrdersByUserId(userId); - } - - @GET - @Path("/status/{status}") - @Operation(summary = "Get orders by status", description = "Returns orders with a specific status") - @APIResponse( - responseCode = "200", - description = "List of orders", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) - ) - ) - public List getOrdersByStatus( - @Parameter(description = "Order status", required = true) - @PathParam("status") String status) { - try { - OrderStatus orderStatus = OrderStatus.valueOf(status.toUpperCase()); - return orderService.getOrdersByStatus(orderStatus); - } catch (IllegalArgumentException e) { - throw new WebApplicationException("Invalid order status: " + status, Response.Status.BAD_REQUEST); - } - } - - @POST - @Operation(summary = "Create new order", description = "Creates a new order with items") - @APIResponse( - responseCode = "201", - description = "Order created", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Order.class) - ) - ) - public Response createOrder( - @Parameter(description = "Order details", required = true) - @NotNull @Valid Order order) { - Order createdOrder = orderService.createOrder(order); - URI location = uriInfo.getAbsolutePathBuilder().path(createdOrder.getOrderId().toString()).build(); - return Response.created(location).entity(createdOrder).build(); - } - - @PUT - @Path("/{id}") - @Operation(summary = "Update order", description = "Updates an existing order") - @APIResponse( - responseCode = "200", - description = "Order updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Order.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order not found" - ) - public Order updateOrder( - @Parameter(description = "ID of the order", required = true) - @PathParam("id") Long id, - @Parameter(description = "Updated order details", required = true) - @NotNull @Valid Order order) { - return orderService.updateOrder(id, order); - } - - @PATCH - @Path("/{id}/status/{status}") - @Operation(summary = "Update order status", description = "Updates the status of an order") - @APIResponse( - responseCode = "200", - description = "Order status updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Order.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Order not found" - ) - @APIResponse( - responseCode = "400", - description = "Invalid order status" - ) - public Order updateOrderStatus( - @Parameter(description = "ID of the order", required = true) - @PathParam("id") Long id, - @Parameter(description = "New order status", required = true) - @PathParam("status") String status) { - try { - OrderStatus orderStatus = OrderStatus.valueOf(status.toUpperCase()); - return orderService.updateOrderStatus(id, orderStatus); - } catch (IllegalArgumentException e) { - throw new WebApplicationException("Invalid order status: " + status, Response.Status.BAD_REQUEST); - } - } - - @DELETE - @Path("/{id}") - @Operation(summary = "Delete order", description = "Deletes an order and its items") - @APIResponse( - responseCode = "204", - description = "Order deleted" - ) - @APIResponse( - responseCode = "404", - description = "Order not found" - ) - public Response deleteOrder( - @Parameter(description = "ID of the order", required = true) - @PathParam("id") Long id) { - orderService.deleteOrder(id); - return Response.noContent().build(); - } -} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java deleted file mode 100644 index 5d3eb30a..00000000 --- a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java +++ /dev/null @@ -1,360 +0,0 @@ -package io.microprofile.tutorial.store.order.service; - -import io.microprofile.tutorial.store.order.entity.Order; -import io.microprofile.tutorial.store.order.entity.OrderItem; -import io.microprofile.tutorial.store.order.entity.OrderStatus; -import io.microprofile.tutorial.store.order.repository.OrderItemRepository; -import io.microprofile.tutorial.store.order.repository.OrderRepository; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response; - -/** - * Service class for Order management operations. - */ -@ApplicationScoped -public class OrderService { - - @Inject - private OrderRepository orderRepository; - - @Inject - private OrderItemRepository orderItemRepository; - - /** - * Creates a new order with items. - * - * @param order The order to create - * @return The created order - */ - @Transactional - public Order createOrder(Order order) { - // Set default values - if (order.getStatus() == null) { - order.setStatus(OrderStatus.CREATED); - } - - order.setCreatedAt(LocalDateTime.now()); - order.setUpdatedAt(LocalDateTime.now()); - - // Calculate total price from order items if not specified - if (order.getTotalPrice() == null || order.getTotalPrice().compareTo(BigDecimal.ZERO) == 0) { - BigDecimal total = order.getOrderItems().stream() - .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - order.setTotalPrice(total); - } - - // Save the order first - Order savedOrder = orderRepository.save(order); - - // Save each order item - if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) { - for (OrderItem item : order.getOrderItems()) { - item.setOrderId(savedOrder.getOrderId()); - orderItemRepository.save(item); - } - } - - // Retrieve the complete order with items - return getOrderById(savedOrder.getOrderId()); - } - - /** - * Gets an order by ID with its items. - * - * @param id The order ID - * @return The order with its items - * @throws WebApplicationException if the order is not found - */ - public Order getOrderById(Long id) { - Order order = orderRepository.findById(id) - .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); - - // Load order items - List items = orderItemRepository.findByOrderId(id); - order.setOrderItems(items); - - return order; - } - - /** - * Gets all orders with their items. - * - * @return A list of all orders with their items - */ - public List getAllOrders() { - List orders = orderRepository.findAll(); - - // Load items for each order - for (Order order : orders) { - List items = orderItemRepository.findByOrderId(order.getOrderId()); - order.setOrderItems(items); - } - - return orders; - } - - /** - * Gets orders by user ID. - * - * @param userId The user ID - * @return A list of orders for the specified user - */ - public List getOrdersByUserId(Long userId) { - List orders = orderRepository.findByUserId(userId); - - // Load items for each order - for (Order order : orders) { - List items = orderItemRepository.findByOrderId(order.getOrderId()); - order.setOrderItems(items); - } - - return orders; - } - - /** - * Gets orders by status. - * - * @param status The order status - * @return A list of orders with the specified status - */ - public List getOrdersByStatus(OrderStatus status) { - List orders = orderRepository.findByStatus(status); - - // Load items for each order - for (Order order : orders) { - List items = orderItemRepository.findByOrderId(order.getOrderId()); - order.setOrderItems(items); - } - - return orders; - } - - /** - * Updates an order. - * - * @param id The order ID - * @param order The updated order information - * @return The updated order - * @throws WebApplicationException if the order is not found - */ - @Transactional - public Order updateOrder(Long id, Order order) { - // Check if order exists - if (!orderRepository.findById(id).isPresent()) { - throw new WebApplicationException("Order not found", Response.Status.NOT_FOUND); - } - - order.setOrderId(id); - order.setUpdatedAt(LocalDateTime.now()); - - // Handle order items if provided - if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) { - // Delete existing items for this order - orderItemRepository.deleteByOrderId(id); - - // Save new items - for (OrderItem item : order.getOrderItems()) { - item.setOrderId(id); - orderItemRepository.save(item); - } - - // Recalculate total price from order items - BigDecimal total = order.getOrderItems().stream() - .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - order.setTotalPrice(total); - } - - // Update the order - Order updatedOrder = orderRepository.update(id, order) - .orElseThrow(() -> new WebApplicationException("Failed to update order", Response.Status.INTERNAL_SERVER_ERROR)); - - // Reload items - List items = orderItemRepository.findByOrderId(id); - updatedOrder.setOrderItems(items); - - return updatedOrder; - } - - /** - * Updates the status of an order. - * - * @param id The order ID - * @param status The new status - * @return The updated order - * @throws WebApplicationException if the order is not found - */ - public Order updateOrderStatus(Long id, OrderStatus status) { - Order order = orderRepository.findById(id) - .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); - - order.setStatus(status); - order.setUpdatedAt(LocalDateTime.now()); - - Order updatedOrder = orderRepository.update(id, order) - .orElseThrow(() -> new WebApplicationException("Failed to update order status", Response.Status.INTERNAL_SERVER_ERROR)); - - // Reload items - List items = orderItemRepository.findByOrderId(id); - updatedOrder.setOrderItems(items); - - return updatedOrder; - } - - /** - * Deletes an order and its items. - * - * @param id The order ID - * @throws WebApplicationException if the order is not found - */ - @Transactional - public void deleteOrder(Long id) { - // Check if order exists - if (!orderRepository.findById(id).isPresent()) { - throw new WebApplicationException("Order not found", Response.Status.NOT_FOUND); - } - - // Delete order items first - orderItemRepository.deleteByOrderId(id); - - // Delete the order - boolean deleted = orderRepository.deleteById(id); - if (!deleted) { - throw new WebApplicationException("Failed to delete order", Response.Status.INTERNAL_SERVER_ERROR); - } - } - - /** - * Gets an order item by ID. - * - * @param id The order item ID - * @return The order item - * @throws WebApplicationException if the order item is not found - */ - public OrderItem getOrderItemById(Long id) { - return orderItemRepository.findById(id) - .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); - } - - /** - * Gets order items by order ID. - * - * @param orderId The order ID - * @return A list of order items for the specified order - */ - public List getOrderItemsByOrderId(Long orderId) { - return orderItemRepository.findByOrderId(orderId); - } - - /** - * Adds an item to an order. - * - * @param orderId The order ID - * @param orderItem The order item to add - * @return The added order item - * @throws WebApplicationException if the order is not found - */ - @Transactional - public OrderItem addOrderItem(Long orderId, OrderItem orderItem) { - // Check if order exists - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); - - orderItem.setOrderId(orderId); - OrderItem savedItem = orderItemRepository.save(orderItem); - - // Update order total price - List items = orderItemRepository.findByOrderId(orderId); - BigDecimal total = items.stream() - .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - order.setTotalPrice(total); - order.setUpdatedAt(LocalDateTime.now()); - orderRepository.update(orderId, order); - - return savedItem; - } - - /** - * Updates an order item. - * - * @param itemId The order item ID - * @param orderItem The updated order item - * @return The updated order item - * @throws WebApplicationException if the order item is not found - */ - @Transactional - public OrderItem updateOrderItem(Long itemId, OrderItem orderItem) { - // Check if item exists - OrderItem existingItem = orderItemRepository.findById(itemId) - .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); - - // Keep the same orderId - orderItem.setOrderItemId(itemId); - orderItem.setOrderId(existingItem.getOrderId()); - - OrderItem updatedItem = orderItemRepository.update(itemId, orderItem) - .orElseThrow(() -> new WebApplicationException("Failed to update order item", Response.Status.INTERNAL_SERVER_ERROR)); - - // Update order total price - Long orderId = updatedItem.getOrderId(); - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.INTERNAL_SERVER_ERROR)); - - List items = orderItemRepository.findByOrderId(orderId); - BigDecimal total = items.stream() - .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - order.setTotalPrice(total); - order.setUpdatedAt(LocalDateTime.now()); - orderRepository.update(orderId, order); - - return updatedItem; - } - - /** - * Deletes an order item. - * - * @param itemId The order item ID - * @throws WebApplicationException if the order item is not found - */ - @Transactional - public void deleteOrderItem(Long itemId) { - // Check if item exists and get its orderId before deletion - OrderItem item = orderItemRepository.findById(itemId) - .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); - - Long orderId = item.getOrderId(); - - // Delete the item - boolean deleted = orderItemRepository.deleteById(itemId); - if (!deleted) { - throw new WebApplicationException("Failed to delete order item", Response.Status.INTERNAL_SERVER_ERROR); - } - - // Update order total price - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.INTERNAL_SERVER_ERROR)); - - List items = orderItemRepository.findByOrderId(orderId); - BigDecimal total = items.stream() - .map(i -> i.getPriceAtOrder().multiply(new BigDecimal(i.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - order.setTotalPrice(total); - order.setUpdatedAt(LocalDateTime.now()); - orderRepository.update(orderId, order); - } -} diff --git a/code/chapter11/order/src/main/liberty/config/server.xml b/code/chapter11/order/src/main/liberty/config/server.xml deleted file mode 100644 index aa667d2b..00000000 --- a/code/chapter11/order/src/main/liberty/config/server.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 - - - - - - \ No newline at end of file diff --git a/code/chapter11/order/src/main/webapp/WEB-INF/web.xml b/code/chapter11/order/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 6a516f16..00000000 --- a/code/chapter11/order/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Order Management - - index.html - - diff --git a/code/chapter11/order/src/main/webapp/index.html b/code/chapter11/order/src/main/webapp/index.html deleted file mode 100644 index 1d427823..00000000 --- a/code/chapter11/order/src/main/webapp/index.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - Order Management Service - - - -

Order Management Service

-

Welcome to the Order Management API, a Jakarta EE and MicroProfile demo.

- -

Available Endpoints:

- -
-

OpenAPI Documentation

-

GET /openapi - Access OpenAPI documentation

- View API Documentation -
- -

Order Operations

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodURLDescription
GET/api/ordersGet all orders
GET/api/orders/{id}Get order by ID
GET/api/orders/user/{userId}Get orders by user ID
GET/api/orders/status/{status}Get orders by status
POST/api/ordersCreate new order
PUT/api/orders/{id}Update order
DELETE/api/orders/{id}Delete order
PATCH/api/orders/{id}/status/{status}Update order status
- -

Order Item Operations

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodURLDescription
GET/api/orderItems/order/{orderId}Get items for an order
GET/api/orderItems/{orderItemId}Get specific order item
POST/api/orderItems/order/{orderId}Add item to order
PUT/api/orderItems/{orderItemId}Update order item
DELETE/api/orderItems/{orderItemId}Delete order item
- -

Example Request

-
curl -X GET http://localhost:8050/order/api/orders
- - diff --git a/code/chapter11/order/src/main/webapp/order-status-codes.html b/code/chapter11/order/src/main/webapp/order-status-codes.html deleted file mode 100644 index faed8a09..00000000 --- a/code/chapter11/order/src/main/webapp/order-status-codes.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - Order Status Codes - - - -

Order Status Codes

-

This page describes the possible status codes for orders in the system.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Status CodeDescription
CREATEDOrder has been created but not yet processed
PAIDPayment has been received for the order
PROCESSINGOrder is being processed (items are being picked, packed, etc.)
SHIPPEDOrder has been shipped to the customer
DELIVEREDOrder has been delivered to the customer
CANCELLEDOrder has been cancelled
- -

Return to main page

- - diff --git a/code/chapter11/payment/Dockerfile b/code/chapter11/payment/Dockerfile deleted file mode 100644 index 77e6dde6..00000000 --- a/code/chapter11/payment/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi - -# Copy configuration files -COPY --chown=1001:0 src/main/liberty/config/ /config/ - -# Create the apps directory and copy the application -COPY --chown=1001:0 target/payment.war /config/apps/ - -# Configure the server to run in production mode -RUN configure.sh - -# Expose the default port -EXPOSE 9050 9443 - -# Set the health check -HEALTHCHECK --start-period=60s --interval=10s --timeout=5s --retries=3 \ - CMD curl -f http://localhost:9050/health || exit 1 - -# Run the server -CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/payment/README.adoc b/code/chapter11/payment/README.adoc deleted file mode 100644 index 500d8075..00000000 --- a/code/chapter11/payment/README.adoc +++ /dev/null @@ -1,266 +0,0 @@ -= Payment Service - -This microservice is part of the Jakarta EE 10 and MicroProfile 6.1-based e-commerce application. It handles payment processing and transaction management. - -== Features - -* Payment transaction processing -* Dynamic configuration management via MicroProfile Config -* RESTful API endpoints with JSON support -* Custom ConfigSource implementation -* OpenAPI documentation - -== Endpoints - -=== GET /payment/api/payment-config -* Returns all current payment configuration values -* Example: `GET http://localhost:9080/payment/api/payment-config` -* Response: `{"gateway.endpoint":"https://api.paymentgateway.com"}` - -=== POST /payment/api/payment-config -* Updates a payment configuration value -* Example: `POST http://localhost:9080/payment/api/payment-config` -* Request body: `{"key": "payment.gateway.endpoint", "value": "https://new-api.paymentgateway.com"}` -* Response: `{"key":"payment.gateway.endpoint","value":"https://new-api.paymentgateway.com","message":"Configuration updated successfully"}` - -=== POST /payment/api/authorize -* Processes a payment -* Example: `POST http://localhost:9080/payment/api/authorize` -* Response: `{"status":"success", "message":"Payment processed successfully."}` - -=== POST /payment/api/payment-config/process-example -* Example endpoint demonstrating payment processing with configuration -* Example: `POST http://localhost:9080/payment/api/payment-config/process-example` -* Request body: `{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}` -* Response: `{"amount":100.00,"message":"Payment processed successfully","status":"success","configUsed":{"gatewayEndpoint":"https://new-api.paymentgateway.com"}}` - -== Building and Running the Service - -=== Prerequisites - -* JDK 17 or higher -* Maven 3.6.0 or higher - -=== Local Development - -[source,bash] ----- -# Build the application -mvn clean package - -# Run the application with Liberty -mvn liberty:run ----- - -The server will start on port 9080 (HTTP) and 9081 (HTTPS). - -=== Docker - -[source,bash] ----- -# Build and run with Docker -./run-docker.sh ----- - -== Project Structure - -* `src/main/java/io/microprofile/tutorial/PaymentRestApplication.java` - Jakarta Restful web service application class -* `src/main/java/io/microprofile/tutorial/store/payment/config/` - Configuration classes -* `src/main/java/io/microprofile/tutorial/store/payment/resource/` - REST resource endpoints -* `src/main/java/io/microprofile/tutorial/store/payment/service/` - Business logic services -* `src/main/java/io/microprofile/tutorial/store/payment/entity/` - Data models -* `src/main/resources/META-INF/services/` - Service provider configuration -* `src/main/liberty/config/` - Liberty server configuration - -== Custom ConfigSource - -The Payment Service implements a custom MicroProfile ConfigSource named `PaymentServiceConfigSource` that provides payment-specific configuration with high priority (ordinal: 600). - -=== Available Configuration Properties - -[cols="1,2,2", options="header"] -|=== -|Property -|Description -|Default Value - -|payment.gateway.endpoint -|Payment gateway endpoint URL -|https://api.paymentgateway.com -|=== - -=== Testing ConfigSource Endpoints - -You can test the ConfigSource endpoints using curl or any REST client: - -[source,bash] ----- -# Get current configuration -curl -s http://localhost:9080/payment/api/payment-config | json_pp - -# Update configuration property -curl -s -X POST -H "Content-Type: application/json" \ - -d '{"key":"payment.gateway.endpoint", "value":"https://new-api.paymentgateway.com"}' \ - http://localhost:9080/payment/api/payment-config | json_pp - -# Test payment processing with the configuration -curl -s -X POST -H "Content-Type: application/json" \ - -d '{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}' \ - http://localhost:9080/payment/api/payment-config/process-example | json_pp - -# Test basic payment authorization -curl -s -X POST -H "Content-Type: application/json" \ - http://localhost:9080/payment/api/authorize | json_pp ----- - -=== Implementation Details - -The custom ConfigSource is implemented in the following classes: - -* `PaymentServiceConfigSource.java` - Implements the MicroProfile ConfigSource interface -* `PaymentConfig.java` - Utility class for accessing configuration properties - -Example usage in application code: - -[source,java] ----- -// Inject standard MicroProfile Config -@Inject -@ConfigProperty(name="payment.gateway.endpoint") -private String endpoint; - -// Or use the utility class -String gatewayUrl = PaymentConfig.getConfigProperty("payment.gateway.endpoint"); ----- - -The custom ConfigSource provides a higher priority (ordinal: 600) than system properties and environment variables, allowing for service-specific defaults while still enabling override via standard mechanisms. - -=== MicroProfile Config Sources - -MicroProfile Config uses a prioritized set of configuration sources. The payment service uses the following configuration sources in order of priority (highest to lowest): - -1. Custom ConfigSource (`PaymentServiceConfigSource`) - Ordinal: 600 -2. System properties - Ordinal: 400 -3. Environment variables - Ordinal: 300 -4. microprofile-config.properties file - Ordinal: 100 - -==== Updating Configuration Values - -You can update configuration properties through different methods: - -===== 1. Using the REST API (runtime) - -This uses the custom ConfigSource and persists only for the current server session: - -[source,bash] ----- -curl -X POST -H "Content-Type: application/json" \ - -d '{"key":"payment.gateway.endpoint", "value":"https://test-api.paymentgateway.com"}' \ - http://localhost:9080/payment/api/payment-config ----- - -===== 2. Using System Properties (startup) - -[source,bash] ----- -# Linux/macOS -mvn liberty:run -Dpayment.gateway.endpoint=https://sys-api.paymentgateway.com - -# Windows -mvn liberty:run "-Dpayment.gateway.endpoint=https://sys-api.paymentgateway.com" ----- - -===== 3. Using Environment Variables (startup) - -Environment variable names must follow the MicroProfile Config convention (uppercase with underscores): - -[source,bash] ----- -# Linux/macOS -export PAYMENT_GATEWAY_ENDPOINT=https://env-api.paymentgateway.com -mvn liberty:run - -# Windows PowerShell -$env:PAYMENT_GATEWAY_ENDPOINT="https://env-api.paymentgateway.com" -mvn liberty:run - -# Windows CMD -set PAYMENT_GATEWAY_ENDPOINT=https://env-api.paymentgateway.com -mvn liberty:run ----- - -===== 4. Using microprofile-config.properties File (build time) - -Edit the file at `src/main/resources/META-INF/microprofile-config.properties`: - -[source,properties] ----- -# Update the endpoint -payment.gateway.endpoint=https://config-api.paymentgateway.com ----- - -Then rebuild and restart the application: - -[source,bash] ----- -mvn clean package liberty:run ----- - -==== Testing Configuration Changes - -After changing a configuration property, you can verify it was updated by calling: - -[source,bash] ----- -curl http://localhost:9080/payment/api/payment-config ----- - -== Documentation - -=== OpenAPI - -The payment service automatically generates OpenAPI documentation using MicroProfile OpenAPI annotations. - -* OpenAPI UI: `http://localhost:9080/payment/api/openapi-ui/` -* OpenAPI JSON: `http://localhost:9080/payment/api/openapi` - -=== MicroProfile Config Specification - -For more information about MicroProfile Config, refer to the official documentation: - -* https://download.eclipse.org/microprofile/microprofile-config-3.1/microprofile-config-spec-3.1.html - -=== Related Resources - -* MicroProfile: https://microprofile.io/ -* Jakarta EE: https://jakarta.ee/ -* Open Liberty: https://openliberty.io/ - -== Troubleshooting - -=== Common Issues - -==== Port Conflicts - -If you encounter a port conflict when starting the server, you can change the ports in the `pom.xml` file: - -[source,xml] ----- -9080 -9081 ----- - -==== ConfigSource Not Loading - -If the custom ConfigSource is not loading, check the following: - -1. Verify the service provider configuration file exists at: - `src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` - -2. Ensure it contains the correct fully qualified class name: - `io.microprofile.tutorial.store.payment.config.PaymentServiceConfigSource` - -==== Deployment Errors - -For CWWKZ0004E deployment errors, check the server logs at: -`target/liberty/wlp/usr/servers/mpServer/logs/messages.log` diff --git a/code/chapter11/payment/pom.xml b/code/chapter11/payment/pom.xml deleted file mode 100644 index 12b8fada..00000000 --- a/code/chapter11/payment/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - 4.0.0 - - io.microprofile.tutorial - payment - 1.0-SNAPSHOT - war - - - - UTF-8 - 17 - 17 - - UTF-8 - UTF-8 - - - 9080 - 9081 - - payment - - - - - - - - - org.projectlombok - lombok - 1.18.26 - provided - - - - - jakarta.platform - jakarta.jakartaee-api - 10.0.0 - provided - - - - - org.eclipse.microprofile - microprofile - 6.1 - pom - provided - - - - junit - junit - 4.11 - test - - - - - ${project.artifactId} - - - - io.openliberty.tools - liberty-maven-plugin - 3.11.2 - - mpServer - - - - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - - - - \ No newline at end of file diff --git a/code/chapter11/payment/run-docker.sh b/code/chapter11/payment/run-docker.sh deleted file mode 100755 index e027bafd..00000000 --- a/code/chapter11/payment/run-docker.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Script to build and run the Payment service in Docker - -# Stop execution on any error -set -e - -# Navigate to the payment service directory -cd "$(dirname "$0")" - -# Build the project with Maven -echo "Building with Maven..." -mvn clean package - -# Build the Docker image -echo "Building Docker image..." -docker build -t payment-service . - -# Run the Docker container -echo "Starting Docker container..." -docker run -d --name payment-service -p 9050:9050 payment-service - -echo "Payment service is running on http://localhost:9050/payment" diff --git a/code/chapter11/payment/run.sh b/code/chapter11/payment/run.sh deleted file mode 100755 index 75fc5f27..00000000 --- a/code/chapter11/payment/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Script to build and run the Payment service - -# Stop execution on any error -set -e - -echo "Building and running Payment service..." - -# Navigate to the payment service directory -cd "$(dirname "$0")" - -# Build the project with Maven -echo "Building with Maven..." -mvn clean package - -# Run the application using Liberty Maven plugin -echo "Starting Liberty server..." -mvn liberty:run diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java deleted file mode 100644 index 9ffd7515..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.microprofile.tutorial; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -@ApplicationPath("/api") -public class PaymentRestApplication extends Application { - // No additional configuration is needed here -} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java deleted file mode 100644 index 5100206e..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.microprofile.tutorial.store.payment.client; - -import io.microprofile.tutorial.store.payment.dto.product.Product; -import jakarta.json.JsonArray; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -public class ProductClient { - public static Product[] getProductsWithJsonb(String targetUrl) { - // This method would typically make a REST call to fetch products. - // For now, we return an empty array as a placeholder. - Client client = ClientBuilder.newClient(); - Response response = client.target(targetUrl) - .request(MediaType.APPLICATION_JSON) - .get(); - - Product[] products = response.readEntity(Product[].class); - response.close(); - client.close(); - - return products; - } - - public static Product[] getProductsWithJsonp(String targetUrl) { - // Default URL for product service - String defaultUrl = "http://localhost:6050/products"; - Client client = ClientBuilder.newClient(); - Response response = client.target(targetUrl != null ? targetUrl : defaultUrl) - .request(MediaType.APPLICATION_JSON) - .get(); - - JsonArray jsonArray = response.readEntity(JsonArray.class); - response.close(); - client.close(); - - return collectProducts(jsonArray); - } - - private static Product[] collectProducts(JsonArray jsonArray) { - Product[] products = new Product[jsonArray.size()]; - for (int i = 0; i < jsonArray.size(); i++) { - Product product = new Product(); - product.setId(jsonArray.getJsonObject(i).getJsonNumber("id").longValue()); - product.setName(jsonArray.getJsonObject(i).getString("name")); - product.setPrice(jsonArray.getJsonObject(i).getJsonNumber("price").doubleValue()); - products[i] = product; - } - return products; - } -} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientJson.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientJson.java deleted file mode 100644 index 32333de3..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientJson.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.microprofile.tutorial.store.payment.client; - -import io.microprofile.tutorial.store.payment.dto.product.Product; -import jakarta.json.JsonArray; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -public class ProductClientJson { - public static Product[] getProductsWithJsonb(String targetUrl) { - // This method would typically make a REST call to fetch products. - // For now, we return an empty array as a placeholder. - Client client = ClientBuilder.newClient(); - Response response = client.target(targetUrl) - .request(MediaType.APPLICATION_JSON) - .get(); - - Product[] products = response.readEntity(Product[].class); - response.close(); - client.close(); - - - return products; - } - - - public static Product[] getProductsWithJsonp(String targetUrl) { - // Default URL for product service - String defaultUrl = "http://localhost:5050/products"; - Client client = ClientBuilder.newClient(); - Response response = client.target(targetUrl != null ? targetUrl : defaultUrl) - .request(MediaType.APPLICATION_JSON) - .get(); - - JsonArray jsonArray = response.readEntity(JsonArray.class); - response.close(); - client.close(); - - return collectProducts(jsonArray); - } - - private static Product[] collectProducts(JsonArray jsonArray) { - Product[] products = new Product[jsonArray.size()]; - for (int i = 0; i < jsonArray.size(); i++) { - Product product = new Product(); - product.setId(jsonArray.getJsonObject(i).getJsonNumber("id").longValue()); - product.setName(jsonArray.getJsonObject(i).getString("name")); - product.setPrice(jsonArray.getJsonObject(i).getJsonNumber("price").doubleValue()); - products[i] = product; - } - return products; - } -} - diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java deleted file mode 100644 index c4df4d6e..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.microprofile.tutorial.store.payment.config; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - -/** - * Utility class for accessing payment service configuration. - */ -public class PaymentConfig { - - private static final Config config = ConfigProvider.getConfig(); - - /** - * Gets a configuration property as a String. - * - * @param key the property key - * @return the property value - */ - public static String getConfigProperty(String key) { - return config.getValue(key, String.class); - } - - /** - * Gets a configuration property as a String with a default value. - * - * @param key the property key - * @param defaultValue the default value if the key doesn't exist - * @return the property value or the default value - */ - public static String getConfigProperty(String key, String defaultValue) { - return config.getOptionalValue(key, String.class).orElse(defaultValue); - } - - /** - * Gets a configuration property as an Integer. - * - * @param key the property key - * @return the property value as an Integer - */ - public static Integer getIntProperty(String key) { - return config.getValue(key, Integer.class); - } - - /** - * Gets a configuration property as a Boolean. - * - * @param key the property key - * @return the property value as a Boolean - */ - public static Boolean getBooleanProperty(String key) { - return config.getValue(key, Boolean.class); - } - - /** - * Updates a configuration property at runtime through the custom ConfigSource. - * - * @param key the property key - * @param value the property value - */ - public static void updateProperty(String key, String value) { - PaymentServiceConfigSource.setProperty(key, value); - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java deleted file mode 100644 index 25b59a4f..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.microprofile.tutorial.store.payment.config; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Custom ConfigSource for Payment Service. - * This config source provides payment-specific configuration with high priority. - */ -public class PaymentServiceConfigSource implements ConfigSource { - - private static final Map properties = new HashMap<>(); - - private static final String NAME = "PaymentServiceConfigSource"; - private static final int ORDINAL = 600; // Higher ordinal means higher priority - - public PaymentServiceConfigSource() { - // Load payment service configurations dynamically - // This example uses hardcoded values for demonstration - properties.put("payment.gateway.endpoint", "https://api.paymentgateway.com"); - } - - @Override - public Map getProperties() { - return properties; - } - - @Override - public Set getPropertyNames() { - return properties.keySet(); - } - - @Override - public String getValue(String propertyName) { - return properties.get(propertyName); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public int getOrdinal() { - return ORDINAL; - } - - /** - * Updates a configuration property at runtime. - * - * @param key the property key - * @param value the property value - */ - public static void setProperty(String key, String value) { - properties.put(key, value); - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java deleted file mode 100644 index 3b588438..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.microprofile.tutorial.store.payment.dto.product; - -import lombok.Data; - -@Data -public class Product { - public Long id; - public String name; - public Double price; -} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java deleted file mode 100644 index 4b62460a..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.microprofile.tutorial.store.payment.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class PaymentDetails { - private String cardNumber; - private String cardHolderName; - private String expiryDate; // Format MM/YY - private String securityCode; - private BigDecimal amount; -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/examples/ProductClientExample.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/examples/ProductClientExample.java deleted file mode 100644 index 0decaa3f..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/examples/ProductClientExample.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.microprofile.tutorial.store.payment.examples; - -import io.microprofile.tutorial.store.payment.client.ProductClientJson; -import io.microprofile.tutorial.store.payment.dto.product.Product; - -import java.util.Arrays; -import java.util.logging.Logger; - -/** - * Example demonstrating how to use the ProductClientJson.getProductsWithJsonp method. - */ -public class ProductClientExample { - - private static final Logger LOGGER = Logger.getLogger(ProductClientExample.class.getName()); - - public static void main(String[] args) { - - // Example 1: Call with default URL (http://localhost:5050/products) - LOGGER.info("=== Example 1: Using default URL ==="); - try { - Product[] products = ProductClientJson.getProductsWithJsonp(null); - printProducts("Default URL", products); - } catch (Exception e) { - LOGGER.warning("Failed to fetch products with default URL: " + e.getMessage()); - } - - // Example 2: Call with custom catalog service URL - LOGGER.info("=== Example 2: Using custom catalog service URL ==="); - try { - String catalogUrl = "http://localhost:5050/catalog/api/products"; - Product[] products = ProductClientJson.getProductsWithJsonp(catalogUrl); - printProducts("Custom catalog URL", products); - } catch (Exception e) { - LOGGER.warning("Failed to fetch products from catalog service: " + e.getMessage()); - } - - // Example 3: Call with different environment URLs - LOGGER.info("=== Example 3: Using different environment URLs ==="); - String[] environmentUrls = { - "http://localhost:5050/catalog/api/products", // Local catalog service - "http://localhost:6050/products", // Alternative port - "https://api.example.com/products" // External API - }; - - for (String url : environmentUrls) { - try { - LOGGER.info("Trying URL: " + url); - Product[] products = ProductClientJson.getProductsWithJsonp(url); - printProducts("URL: " + url, products); - break; // Stop on first successful call - } catch (Exception e) { - LOGGER.warning("Failed to fetch from " + url + ": " + e.getMessage()); - } - } - } - - /** - * Helper method to print product information - */ - private static void printProducts(String source, Product[] products) { - LOGGER.info("Products from " + source + ":"); - if (products != null && products.length > 0) { - Arrays.stream(products) - .forEach(product -> LOGGER.info(" " + product.toString())); - LOGGER.info("Total products found: " + products.length); - } else { - LOGGER.info(" No products found"); - } - System.out.println(); // Add blank line for readability - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java deleted file mode 100644 index 6a4002f3..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.microprofile.tutorial.store.payment.resource; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import java.util.HashMap; -import java.util.Map; - -import io.microprofile.tutorial.store.payment.config.PaymentConfig; -import io.microprofile.tutorial.store.payment.entity.PaymentDetails; - -/** - * Resource to demonstrate the use of the custom ConfigSource. - */ -@ApplicationScoped -@Path("/payment-config") -public class PaymentConfigResource { - - /** - * Get all payment configuration properties. - * - * @return Response with payment configuration - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - public Response getPaymentConfig() { - Map configValues = new HashMap<>(); - - // Retrieve values from our custom ConfigSource - configValues.put("gateway.endpoint", PaymentConfig.getConfigProperty("payment.gateway.endpoint")); - - return Response.ok(configValues).build(); - } - - /** - * Update a payment configuration property. - * - * @param configUpdate Map containing the key and value to update - * @return Response indicating success - */ - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Response updatePaymentConfig(Map configUpdate) { - String key = configUpdate.get("key"); - String value = configUpdate.get("value"); - - if (key == null || value == null) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("Both 'key' and 'value' must be provided").build(); - } - - // Only allow updating specific payment properties - if (!key.startsWith("payment.")) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("Only payment configuration properties can be updated").build(); - } - - // Update the property in our custom ConfigSource - PaymentConfig.updateProperty(key, value); - - return Response.ok(Map.of("message", "Configuration updated successfully", - "key", key, "value", value)).build(); - } - - /** - * Example of how to use the payment configuration in a real payment processing method. - * - * @param paymentDetails Payment details for processing - * @return Response with payment result - */ - @POST - @Path("/process-example") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Response processPaymentExample(PaymentDetails paymentDetails) { - // Using configuration values in payment processing logic - String gatewayEndpoint = PaymentConfig.getConfigProperty("payment.gateway.endpoint"); - - // This is just for demonstration - in a real implementation, - // we would use these values to configure the payment gateway client - Map result = new HashMap<>(); - result.put("status", "success"); - result.put("message", "Payment processed successfully"); - result.put("amount", paymentDetails.getAmount()); - result.put("configUsed", Map.of( - "gatewayEndpoint", gatewayEndpoint - )); - - return Response.ok(result).build(); - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java deleted file mode 100644 index 9d1069ee..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java +++ /dev/null @@ -1,186 +0,0 @@ -package io.microprofile.tutorial.store.payment.resource; - -import io.microprofile.tutorial.store.payment.client.ProductClientJson; -import io.microprofile.tutorial.store.payment.dto.product.Product; -import io.microprofile.tutorial.store.payment.service.ProductIntegrationService; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -/** - * REST resource demonstrating how to use ProductClientJson.getProductsWithJsonp - * within REST endpoints for the Payment service. - */ -@ApplicationScoped -@Path("/products") -public class PaymentProductResource { - - private static final Logger LOGGER = Logger.getLogger(PaymentProductResource.class.getName()); - - @Inject - private ProductIntegrationService productService; - - @Inject - @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog/api/products") - private String catalogServiceUrl; - - /** - * Gets all products available for payment processing. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Get all products", description = "Retrieves all products available for payment processing") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Products retrieved successfully"), - @APIResponse(responseCode = "500", description = "Failed to retrieve products") - }) - public Response getAllProducts() { - LOGGER.info("REST: Fetching all products for payment processing"); - - try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); - - if (products != null) { - LOGGER.info("Successfully retrieved " + products.length + " products"); - return Response.ok(products).build(); - } else { - LOGGER.warning("No products returned from catalog service"); - return Response.ok(new Product[0]).build(); - } - - } catch (Exception e) { - LOGGER.severe("Failed to retrieve products: " + e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Failed to retrieve products", "message", e.getMessage())) - .build(); - } - } - - /** - * Gets products from a specific catalog service URL. - */ - @GET - @Path("/from-url") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Get products from specific URL", description = "Retrieves products from a specified catalog service URL") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Products retrieved successfully"), - @APIResponse(responseCode = "400", description = "Invalid URL provided"), - @APIResponse(responseCode = "500", description = "Failed to retrieve products") - }) - public Response getProductsFromUrl( - @Parameter(description = "Catalog service URL", required = true) - @QueryParam("url") String catalogUrl) { - - if (catalogUrl == null || catalogUrl.trim().isEmpty()) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(Map.of("error", "URL parameter is required")) - .build(); - } - - LOGGER.info("REST: Fetching products from URL: " + catalogUrl); - - try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogUrl); - - Map result = new HashMap<>(); - result.put("sourceUrl", catalogUrl); - result.put("productCount", products != null ? products.length : 0); - result.put("products", products != null ? products : new Product[0]); - - return Response.ok(result).build(); - - } catch (Exception e) { - LOGGER.severe("Failed to retrieve products from " + catalogUrl + ": " + e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of( - "error", "Failed to retrieve products", - "url", catalogUrl, - "message", e.getMessage())) - .build(); - } - } - - /** - * Validates if a product is available for payment processing. - */ - @GET - @Path("/{productId}/validate") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Validate product for payment", description = "Validates if a product is available for payment processing") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Product validation completed"), - @APIResponse(responseCode = "404", description = "Product not found") - }) - public Response validateProduct( - @Parameter(description = "Product ID", required = true) - @PathParam("productId") Long productId) { - - LOGGER.info("REST: Validating product ID: " + productId); - - boolean isValid = productService.validateProductForPayment(productId); - Product productDetails = productService.getProductDetails(productId); - - Map result = new HashMap<>(); - result.put("productId", productId); - result.put("isValid", isValid); - result.put("availableForPayment", isValid); - - if (productDetails != null) { - result.put("product", productDetails); - } - - Response.Status status = isValid ? Response.Status.OK : Response.Status.NOT_FOUND; - return Response.status(status).entity(result).build(); - } - - /** - * Gets products within a specific price range. - */ - @GET - @Path("/price-range") - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Get products by price range", description = "Retrieves products within a specified price range") - @APIResponses({ - @APIResponse(responseCode = "200", description = "Products retrieved successfully"), - @APIResponse(responseCode = "400", description = "Invalid price range") - }) - public Response getProductsByPriceRange( - @Parameter(description = "Minimum price", required = true) - @QueryParam("minPrice") @DefaultValue("0") double minPrice, - @Parameter(description = "Maximum price", required = true) - @QueryParam("maxPrice") @DefaultValue("1000") double maxPrice) { - - if (minPrice < 0 || maxPrice < 0 || minPrice > maxPrice) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(Map.of("error", "Invalid price range. minPrice and maxPrice must be >= 0 and minPrice <= maxPrice")) - .build(); - } - - LOGGER.info(String.format("REST: Fetching products in price range: $%.2f - $%.2f", minPrice, maxPrice)); - - List products = productService.getProductsByPriceRange(minPrice, maxPrice); - - Map result = new HashMap<>(); - result.put("minPrice", minPrice); - result.put("maxPrice", maxPrice); - result.put("productCount", products.size()); - result.put("products", products); - - return Response.ok(result).build(); - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java deleted file mode 100644 index 7e7c6d2d..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.microprofile.tutorial.store.payment.service; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; - - -@RequestScoped -@Path("/authorize") -public class PaymentService { - - @Inject - @ConfigProperty(name = "payment.gateway.endpoint") - private String endpoint; - - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Process payment", description = "Process payment using the payment gateway API") - @APIResponses(value = { - @APIResponse(responseCode = "200", description = "Payment processed successfully"), - @APIResponse(responseCode = "400", description = "Invalid input data"), - @APIResponse(responseCode = "500", description = "Internal server error") - }) - public Response processPayment() { - - // Example logic to call the payment gateway API - System.out.println("Calling payment gateway API at: " + endpoint); - // Assuming a successful payment operation for demonstration purposes - // Actual implementation would involve calling the payment gateway and handling the response - - // Dummy response for successful payment processing - String result = "{\"status\":\"success\", \"message\":\"Payment processed successfully.\"}"; - return Response.ok(result, MediaType.APPLICATION_JSON).build(); - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java deleted file mode 100644 index 884769eb..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.microprofile.tutorial.store.payment.service; - -import io.microprofile.tutorial.store.payment.client.ProductClientJson; -import io.microprofile.tutorial.store.payment.dto.product.Product; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * Service for integrating with the Product/Catalog service. - * Provides business logic for product validation and retrieval in the context of payment processing. - */ -@ApplicationScoped -public class ProductIntegrationService { - - private static final Logger LOGGER = Logger.getLogger(ProductIntegrationService.class.getName()); - - @Inject - @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog/api/products") - private String catalogServiceUrl; - - /** - * Validates if a product is suitable for payment processing. - * - * @param productId The product ID to validate - * @return true if the product is valid for payment, false otherwise - */ - public boolean validateProductForPayment(Long productId) { - LOGGER.info("Validating product for payment: " + productId); - - try { - Product product = getProductDetails(productId); - if (product == null) { - LOGGER.warning("Product not found: " + productId); - return false; - } - - // Basic validation: product must have a valid price and name - boolean isValid = product.price != null && - product.price > 0.0 && - product.name != null && - !product.name.trim().isEmpty(); - - LOGGER.info("Product " + productId + " validation result: " + isValid); - return isValid; - - } catch (Exception e) { - LOGGER.severe("Error validating product " + productId + ": " + e.getMessage()); - return false; - } - } - - /** - * Gets detailed information about a specific product. - * - * @param productId The product ID - * @return Product details or null if not found - */ - public Product getProductDetails(Long productId) { - LOGGER.info("Fetching product details for ID: " + productId); - - try { - Product[] allProducts = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); - - if (allProducts != null) { - for (Product product : allProducts) { - if (product.id.equals(productId)) { - LOGGER.info("Found product: " + product.name + " (ID: " + productId + ")"); - return product; - } - } - } - - LOGGER.warning("Product not found with ID: " + productId); - return null; - - } catch (Exception e) { - LOGGER.severe("Error fetching product details for ID " + productId + ": " + e.getMessage()); - return null; - } - } - - /** - * Gets products within a specified price range. - * - * @param minPrice Minimum price (inclusive) - * @param maxPrice Maximum price (inclusive) - * @return List of products within the price range - */ - public List getProductsByPriceRange(double minPrice, double maxPrice) { - LOGGER.info("Fetching products in price range: " + minPrice + " - " + maxPrice); - - try { - Product[] allProducts = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); - - if (allProducts == null) { - LOGGER.warning("No products returned from catalog service"); - return new ArrayList<>(); - } - - List filteredProducts = Arrays.stream(allProducts) - .filter(product -> product.price != null) - .filter(product -> product.price >= minPrice) - .filter(product -> product.price <= maxPrice) - .collect(Collectors.toList()); - - LOGGER.info("Found " + filteredProducts.size() + " products in price range"); - return filteredProducts; - - } catch (Exception e) { - LOGGER.severe("Error fetching products by price range: " + e.getMessage()); - return new ArrayList<>(); - } - } - - /** - * Gets all available products. - * - * @return Array of all products - */ - public Product[] getAllProducts() { - LOGGER.info("Fetching all products"); - - try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); - - if (products != null) { - LOGGER.info("Retrieved " + products.length + " products"); - } else { - LOGGER.warning("No products returned from catalog service"); - products = new Product[0]; - } - - return products; - - } catch (Exception e) { - LOGGER.severe("Error fetching all products: " + e.getMessage()); - return new Product[0]; - } - } -} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http deleted file mode 100644 index 98ae2e52..00000000 --- a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http +++ /dev/null @@ -1,9 +0,0 @@ -POST https://orange-zebra-r745vp6rjcxp67-9080.app.github.dev/payment/authorize - -{ - "cardNumber": "4111111111111111", - "cardHolderName": "John Doe", - "expiryDate": "12/25", - "securityCode": "123", - "amount": 100.00 -} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/liberty/config/server.xml b/code/chapter11/payment/src/main/liberty/config/server.xml deleted file mode 100644 index 8204434e..00000000 --- a/code/chapter11/payment/src/main/liberty/config/server.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - jakartaEE-10.0 - microProfile-6.1 - restfulWS - jsonp - jsonb - cdi - mpConfig - mpOpenAPI - mpJwt - - - - - - \ No newline at end of file diff --git a/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index cf834366..00000000 --- a/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,11 +0,0 @@ -# microprofile-config.properties -mp.openapi.scan=true -product.maintenanceMode=false - -# Product Service Configuration -payment.gateway.endpoint=https://api.paymentgateway.com/v1 - -# Catalog Service Configuration for ProductClientJson -catalog.service.url=http://localhost:5050/catalog/api/products -catalog.service.fallback.url=http://localhost:6050/products -catalog.service.timeout=5000 \ No newline at end of file diff --git a/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource deleted file mode 100644 index 98707178..00000000 --- a/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource +++ /dev/null @@ -1 +0,0 @@ -io.microprofile.tutorial.store.payment.config.PaymentServiceConfigSource \ No newline at end of file diff --git a/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 9e4411be..00000000 --- a/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Payment Service - - - index.html - index.jsp - - diff --git a/code/chapter11/payment/src/main/webapp/index.html b/code/chapter11/payment/src/main/webapp/index.html deleted file mode 100644 index 33086f26..00000000 --- a/code/chapter11/payment/src/main/webapp/index.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - Payment Service - MicroProfile Config Demo - - - -
-

Payment Service

-

MicroProfile Config Demo with Custom ConfigSource

-
- -
-
-

About this Service

-

The Payment Service demonstrates MicroProfile Config integration with custom ConfigSource implementation.

-

It provides endpoints for managing payment configuration and processing payments using dynamic configuration.

-

Key Features:

-
    -
  • Custom MicroProfile ConfigSource with ordinal 600 (highest priority)
  • -
  • Dynamic configuration updates via REST API
  • -
  • Payment gateway endpoint configuration
  • -
  • Real-time configuration access for payment processing
  • -
-
- -
-

API Endpoints

-
    -
  • GET /api/payment-config - Get current payment configuration
  • -
  • POST /api/payment-config - Update payment configuration property
  • -
  • POST /api/authorize - Process payment authorization
  • -
  • POST /api/payment-config/process-example - Example payment processing with config
  • -
-
- -
-

Configuration Management

-

This service implements a custom MicroProfile ConfigSource that allows dynamic configuration updates:

-
    -
  • Configuration Priority: Custom ConfigSource (600) > System Properties (400) > Environment Variables (300) > microprofile-config.properties (100)
  • -
  • Current Properties: payment.gateway.endpoint
  • -
  • Update Method: POST to /api/payment-config with {"key": "payment.property.name", "value": "new-value"}
  • -
-
- - -
- -
-

MicroProfile Config Demo | Payment Service

-

Powered by Open Liberty & MicroProfile Config 3.0

-
- - diff --git a/code/chapter11/payment/src/main/webapp/index.jsp b/code/chapter11/payment/src/main/webapp/index.jsp deleted file mode 100644 index d5de5cb2..00000000 --- a/code/chapter11/payment/src/main/webapp/index.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> - - - - - - Redirecting... - - -

Redirecting to the Payment Service homepage...

- - diff --git a/code/chapter11/run-all-services.sh b/code/chapter11/run-all-services.sh deleted file mode 100755 index 6ee8091b..00000000 --- a/code/chapter11/run-all-services.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Build all projects -echo "Building User Service..." -cd user && mvn clean package && cd .. - -echo "Building Inventory Service..." -cd inventory && mvn clean package && cd .. - -echo "Building Order Service..." -cd order && mvn clean package && cd .. - -echo "Building Catalog Service..." -cd catalog && mvn clean package && cd .. - -echo "Building Payment Service..." -cd payment && mvn clean package && cd .. - -echo "Building Shopping Cart Service..." -cd shoppingcart && mvn clean package && cd .. - -echo "Building Shipment Service..." -cd shipment && mvn clean package && cd .. - -# Start all services using docker-compose -echo "Starting all services with Docker Compose..." -docker-compose up -d - -echo "All services are running:" -echo "- User Service: https:///user" -echo "- Inventory Service: https:///inventory" -echo "- Order Service: https:///order" -echo "- Catalog Service: https:///catalog" -echo "- Payment Service: https:///payment" -echo "- Shopping Cart Service: https:///shoppingcart" -echo "- Shipment Service: https:///shipment" diff --git a/code/chapter11/shipment/Dockerfile b/code/chapter11/shipment/Dockerfile deleted file mode 100644 index 287b43d0..00000000 --- a/code/chapter11/shipment/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM icr.io/appcafe/open-liberty:23.0.0.3-full-java17-openj9-ubi - -# Copy config -COPY --chown=1001:0 src/main/liberty/config/ /config/ - -# Create the app directory -COPY --chown=1001:0 target/shipment.war /config/apps/ - -# Optional: Copy utility scripts -COPY --chown=1001:0 *.sh /opt/ol/helpers/ - -# Environment variables -ENV VERBOSE=true - -# This is important - adds the management of vulnerability databases to allow Docker scanning -RUN dnf install -y shadow-utils - -# Set environment variable for MP config profile -ENV MP_CONFIG_PROFILE=docker - -EXPOSE 8060 9060 - -# Run as non-root user for security -USER 1001 - -# Start Liberty -ENTRYPOINT ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/shipment/README.md b/code/chapter11/shipment/README.md deleted file mode 100644 index 41619949..00000000 --- a/code/chapter11/shipment/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Shipment Service - -This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. - -## Overview - -The Shipment Service is responsible for: -- Creating shipments for orders -- Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) -- Assigning tracking numbers -- Estimating delivery dates -- Communicating with the Order Service to update order status - -## Technologies - -The Shipment Service is built using: -- Jakarta EE 10 -- MicroProfile 6.1 -- Open Liberty -- Java 17 - -## Getting Started - -### Prerequisites - -- JDK 17+ -- Maven 3.8+ -- Docker (for containerized deployment) - -### Running Locally - -To build and run the service: - -```bash -./run.sh -``` - -This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment - -### Running with Docker - -To build and run the service in a Docker container: - -```bash -./run-docker.sh -``` - -This will build a Docker image for the service and run it, exposing ports 8060 and 9060. - -## API Endpoints - -| Method | URL | Description | -|--------|-------------------------------------------|--------------------------------------| -| POST | /api/shipments/orders/{orderId} | Create a new shipment | -| GET | /api/shipments/{shipmentId} | Get a shipment by ID | -| GET | /api/shipments | Get all shipments | -| GET | /api/shipments/status/{status} | Get shipments by status | -| GET | /api/shipments/orders/{orderId} | Get shipments for an order | -| GET | /api/shipments/tracking/{trackingNumber} | Get a shipment by tracking number | -| PUT | /api/shipments/{shipmentId}/status/{status} | Update shipment status | -| PUT | /api/shipments/{shipmentId}/carrier | Update shipment carrier | -| PUT | /api/shipments/{shipmentId}/tracking | Update shipment tracking number | -| PUT | /api/shipments/{shipmentId}/delivery-date | Update estimated delivery date | -| PUT | /api/shipments/{shipmentId}/notes | Update shipment notes | -| DELETE | /api/shipments/{shipmentId} | Delete a shipment | - -## MicroProfile Features - -The service utilizes several MicroProfile features: - -- **Config**: For external configuration -- **Health**: For liveness and readiness checks -- **Metrics**: For monitoring service performance -- **Fault Tolerance**: For resilient communication with the Order Service -- **OpenAPI**: For API documentation - -## Documentation - -API documentation is available at: -- OpenAPI: http://localhost:8060/shipment/openapi -- Swagger UI: http://localhost:8060/shipment/openapi/ui - -## Monitoring - -Health and metrics endpoints: -- Health: http://localhost:8060/shipment/health -- Metrics: http://localhost:8060/shipment/metrics diff --git a/code/chapter11/shipment/pom.xml b/code/chapter11/shipment/pom.xml deleted file mode 100644 index 9a78242c..00000000 --- a/code/chapter11/shipment/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - io.microprofile - shipment - 1.0-SNAPSHOT - war - - shipment-service - https://microprofile.io - - - UTF-8 - 17 - 10.0.0 - 6.1 - 23.0.0.3 - 1.18.24 - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - shipment - - - - io.openliberty.tools - liberty-maven-plugin - 3.8.2 - - shipmentServer - runnable - 120 - - /shipment - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/shipment/run-docker.sh b/code/chapter11/shipment/run-docker.sh deleted file mode 100755 index 69a51506..00000000 --- a/code/chapter11/shipment/run-docker.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Build and run the Shipment Service in Docker -echo "Building and starting Shipment Service in Docker..." - -# Build the application -mvn clean package - -# Build and run the Docker image -docker build -t shipment-service . -docker run -p 8060:8060 -p 9060:9060 --name shipment-service shipment-service diff --git a/code/chapter11/shipment/run.sh b/code/chapter11/shipment/run.sh deleted file mode 100755 index b6fd34ac..00000000 --- a/code/chapter11/shipment/run.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Build and run the Shipment Service -echo "Building and starting Shipment Service..." - -# Stop running server if already running -if [ -f target/liberty/wlp/usr/servers/shipmentServer/workarea/.sRunning ]; then - mvn liberty:stop -fi - -# Clean, build and run -mvn clean package liberty:run diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java deleted file mode 100644 index 3f7288ba..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.microprofile.tutorial.store.shipment; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; -import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; -import org.eclipse.microprofile.openapi.annotations.info.Contact; -import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.info.License; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * JAX-RS Application class for the shipment service. - */ -@ApplicationPath("/api") -@OpenAPIDefinition( - info = @Info( - title = "Shipment Service API", - version = "1.0.0", - description = "API for managing shipments in the microprofile tutorial store", - contact = @Contact( - name = "Shipment Service Support", - email = "shipment@example.com" - ), - license = @License( - name = "Apache 2.0", - url = "https://www.apache.org/licenses/LICENSE-2.0.html" - ) - ), - tags = { - @Tag(name = "Shipment Resource", description = "Operations for managing shipments") - } -) -public class ShipmentApplication extends Application { - // Empty application class, all configuration is provided by annotations -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java deleted file mode 100644 index ba3ce1f8..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java +++ /dev/null @@ -1,193 +0,0 @@ -package io.microprofile.tutorial.store.shipment.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Order Service. - */ -@ApplicationScoped -public class OrderClient { - - private static final Logger LOGGER = Logger.getLogger(OrderClient.class.getName()); - - @ConfigProperty(name = "order.service.url", defaultValue = "http://localhost:8050/order") - private String orderServiceUrl; - - /** - * Updates the order status after a shipment has been processed. - * - * @param orderId The ID of the order to update - * @param newStatus The new status for the order - * @return true if the update was successful, false otherwise - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "updateOrderStatusFallback") - public boolean updateOrderStatus(Long orderId, String newStatus) { - LOGGER.info(String.format("Updating order %d status to %s", orderId, newStatus)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d/status/%s", orderServiceUrl, orderId, newStatus); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .put(Entity.json("{}")); - - boolean success = response.getStatus() == Response.Status.OK.getStatusCode(); - if (!success) { - LOGGER.warning(String.format("Failed to update order status. Status code: %d", response.getStatus())); - } - return success; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Verifies that an order exists and is in a valid state for shipment. - * - * @param orderId The ID of the order to verify - * @return true if the order exists and is in a valid state, false otherwise - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "verifyOrderFallback") - public boolean verifyOrder(Long orderId) { - LOGGER.info(String.format("Verifying order %d for shipment", orderId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple check if the order is in a valid state for shipment - // In a real app, we'd parse the JSON properly - return jsonResponse.contains("\"status\":\"PAID\"") || - jsonResponse.contains("\"status\":\"PROCESSING\"") || - jsonResponse.contains("\"status\":\"READY_FOR_SHIPMENT\""); - } - - LOGGER.warning(String.format("Failed to verify order. Status code: %d", response.getStatus())); - return false; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Gets the shipping address for an order. - * - * @param orderId The ID of the order - * @return The shipping address, or null if not found - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "getShippingAddressFallback") - public String getShippingAddress(Long orderId) { - LOGGER.info(String.format("Getting shipping address for order %d", orderId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple extract of shipping address - in real app use proper JSON parsing - if (jsonResponse.contains("\"shippingAddress\":")) { - int startIndex = jsonResponse.indexOf("\"shippingAddress\":") + "\"shippingAddress\":".length(); - startIndex = jsonResponse.indexOf("\"", startIndex) + 1; - int endIndex = jsonResponse.indexOf("\"", startIndex); - if (endIndex > startIndex) { - return jsonResponse.substring(startIndex, endIndex); - } - } - } - - LOGGER.warning(String.format("Failed to get shipping address. Status code: %d", response.getStatus())); - return null; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for updateOrderStatus. - * - * @param orderId The ID of the order - * @param newStatus The new status for the order - * @return false, indicating failure - */ - public boolean updateOrderStatusFallback(Long orderId, String newStatus) { - LOGGER.warning(String.format("Using fallback for order status update. Order ID: %d, Status: %s", orderId, newStatus)); - return false; - } - - /** - * Fallback method for verifyOrder. - * - * @param orderId The ID of the order - * @return false, indicating failure - */ - public boolean verifyOrderFallback(Long orderId) { - LOGGER.warning(String.format("Using fallback for order verification. Order ID: %d", orderId)); - return false; - } - - /** - * Fallback method for getShippingAddress. - * - * @param orderId The ID of the order - * @return null, indicating failure - */ - public String getShippingAddressFallback(Long orderId) { - LOGGER.warning(String.format("Using fallback for getting shipping address. Order ID: %d", orderId)); - return null; - } -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java deleted file mode 100644 index d9bea89e..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.microprofile.tutorial.store.shipment.entity; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * Shipment class for the microprofile tutorial store application. - * This class represents a shipment of an order in the system. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Shipment { - - private Long shipmentId; - - @NotNull(message = "Order ID cannot be null") - private Long orderId; - - private String trackingNumber; - - @NotNull(message = "Status cannot be null") - private ShipmentStatus status; - - private LocalDateTime estimatedDelivery; - - private LocalDateTime shippedAt; - - @Builder.Default - private LocalDateTime createdAt = LocalDateTime.now(); - - private LocalDateTime updatedAt; - - private String carrier; - - private String shippingAddress; - - private String notes; -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java deleted file mode 100644 index 0e120a9b..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.microprofile.tutorial.store.shipment.entity; - -/** - * ShipmentStatus enum for the microprofile tutorial store application. - * This enum defines the possible statuses for a shipment. - */ -public enum ShipmentStatus { - PENDING, // Shipment is pending - PROCESSING, // Shipment is being processed - SHIPPED, // Shipment has been shipped - IN_TRANSIT, // Shipment is in transit - OUT_FOR_DELIVERY,// Shipment is out for delivery - DELIVERED, // Shipment has been delivered - FAILED, // Shipment delivery failed - RETURNED // Shipment was returned -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java deleted file mode 100644 index ec26495e..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.microprofile.tutorial.store.shipment.filter; - -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Filter to enable CORS for the Shipment service. - */ -public class CorsFilter implements Filter { - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // No initialization required - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - - // Allow requests from any origin - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - response.setHeader("Access-Control-Max-Age", "3600"); - - // For preflight requests - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { - response.setStatus(HttpServletResponse.SC_OK); - } else { - chain.doFilter(request, response); - } - } - - @Override - public void destroy() { - // No cleanup required - } -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java deleted file mode 100644 index 4bf8a503..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.microprofile.tutorial.store.shipment.health; - -import io.microprofile.tutorial.store.shipment.client.OrderClient; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.health.HealthCheck; -import org.eclipse.microprofile.health.HealthCheckResponse; -import org.eclipse.microprofile.health.Liveness; -import org.eclipse.microprofile.health.Readiness; - -/** - * Health check for the shipment service. - */ -@ApplicationScoped -public class ShipmentHealthCheck { - - @Inject - private OrderClient orderClient; - - /** - * Liveness check for the shipment service. - * Verifies that the application is running and not in a failed state. - * - * @return HealthCheckResponse indicating whether the service is live - */ - @Liveness - @ApplicationScoped - public static class LivenessCheck implements HealthCheck { - @Override - public HealthCheckResponse call() { - return HealthCheckResponse.named("shipment-liveness") - .up() - .withData("memory", Runtime.getRuntime().freeMemory()) - .build(); - } - } - - /** - * Readiness check for the shipment service. - * Verifies that the service is ready to handle requests, including connectivity to dependencies. - * - * @return HealthCheckResponse indicating whether the service is ready - */ - @Readiness - @ApplicationScoped - public class ReadinessCheck implements HealthCheck { - @Override - public HealthCheckResponse call() { - boolean orderServiceReachable = false; - - try { - // Simple check to see if the Order service is reachable - // We use a dummy order ID just to test connectivity - orderClient.getShippingAddress(999999L); - orderServiceReachable = true; - } catch (Exception e) { - // If the order service is not reachable, the health check will fail - orderServiceReachable = false; - } - - return HealthCheckResponse.named("shipment-readiness") - .status(orderServiceReachable) - .withData("orderServiceReachable", orderServiceReachable) - .build(); - } - } -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java deleted file mode 100644 index c4013a92..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.microprofile.tutorial.store.shipment.repository; - -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Simple in-memory repository for Shipment objects. - * This class provides CRUD operations for Shipment entities. - */ -@ApplicationScoped -public class ShipmentRepository { - - private final Map shipments = new ConcurrentHashMap<>(); - private long nextId = 1; - - /** - * Saves a shipment to the repository. - * If the shipment has no ID, a new ID is assigned. - * - * @param shipment The shipment to save - * @return The saved shipment with ID assigned - */ - public Shipment save(Shipment shipment) { - if (shipment.getShipmentId() == null) { - shipment.setShipmentId(nextId++); - } - - if (shipment.getCreatedAt() == null) { - shipment.setCreatedAt(LocalDateTime.now()); - } - - shipment.setUpdatedAt(LocalDateTime.now()); - - shipments.put(shipment.getShipmentId(), shipment); - return shipment; - } - - /** - * Finds a shipment by ID. - * - * @param id The shipment ID - * @return An Optional containing the shipment if found, or empty if not found - */ - public Optional findById(Long id) { - return Optional.ofNullable(shipments.get(id)); - } - - /** - * Finds shipments by order ID. - * - * @param orderId The order ID - * @return A list of shipments for the specified order - */ - public List findByOrderId(Long orderId) { - return shipments.values().stream() - .filter(shipment -> shipment.getOrderId().equals(orderId)) - .collect(Collectors.toList()); - } - - /** - * Finds shipments by tracking number. - * - * @param trackingNumber The tracking number - * @return A list of shipments with the specified tracking number - */ - public List findByTrackingNumber(String trackingNumber) { - return shipments.values().stream() - .filter(shipment -> trackingNumber.equals(shipment.getTrackingNumber())) - .collect(Collectors.toList()); - } - - /** - * Finds shipments by status. - * - * @param status The shipment status - * @return A list of shipments with the specified status - */ - public List findByStatus(ShipmentStatus status) { - return shipments.values().stream() - .filter(shipment -> shipment.getStatus() == status) - .collect(Collectors.toList()); - } - - /** - * Finds shipments that are expected to be delivered by a certain date. - * - * @param deliveryDate The delivery date - * @return A list of shipments expected to be delivered by the specified date - */ - public List findByEstimatedDeliveryBefore(LocalDateTime deliveryDate) { - return shipments.values().stream() - .filter(shipment -> shipment.getEstimatedDelivery() != null && - shipment.getEstimatedDelivery().isBefore(deliveryDate)) - .collect(Collectors.toList()); - } - - /** - * Retrieves all shipments from the repository. - * - * @return A list of all shipments - */ - public List findAll() { - return new ArrayList<>(shipments.values()); - } - - /** - * Deletes a shipment by ID. - * - * @param id The ID of the shipment to delete - * @return true if the shipment was deleted, false if not found - */ - public boolean deleteById(Long id) { - return shipments.remove(id) != null; - } - - /** - * Updates an existing shipment. - * - * @param id The ID of the shipment to update - * @param shipment The updated shipment information - * @return An Optional containing the updated shipment, or empty if not found - */ - public Optional update(Long id, Shipment shipment) { - if (!shipments.containsKey(id)) { - return Optional.empty(); - } - - // Preserve creation date - LocalDateTime createdAt = shipments.get(id).getCreatedAt(); - shipment.setCreatedAt(createdAt); - - shipment.setShipmentId(id); - shipment.setUpdatedAt(LocalDateTime.now()); - - shipments.put(id, shipment); - return Optional.of(shipment); - } -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java deleted file mode 100644 index 602be808..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java +++ /dev/null @@ -1,397 +0,0 @@ -package io.microprofile.tutorial.store.shipment.resource; - -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; -import io.microprofile.tutorial.store.shipment.service.ShipmentService; -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * REST resource for shipment operations. - */ -@Path("/api/shipments") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Shipment Resource", description = "Operations for managing shipments") -public class ShipmentResource { - - private static final Logger LOGGER = Logger.getLogger(ShipmentResource.class.getName()); - - @Inject - private ShipmentService shipmentService; - - /** - * Creates a new shipment for an order. - * - * @param orderId The order ID - * @return The created shipment - */ - @POST - @Path("/orders/{orderId}") - @Operation(summary = "Create a new shipment for an order") - @APIResponse(responseCode = "201", description = "Shipment created", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "400", description = "Invalid order ID") - @APIResponse(responseCode = "404", description = "Order not found or not ready for shipment") - public Response createShipment( - @Parameter(description = "Order ID", required = true) - @PathParam("orderId") Long orderId) { - - LOGGER.info("REST request to create shipment for order: " + orderId); - - Shipment shipment = shipmentService.createShipment(orderId); - if (shipment == null) { - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Order not found or not ready for shipment\"}") - .build(); - } - - return Response.status(Response.Status.CREATED) - .entity(shipment) - .build(); - } - - /** - * Gets a shipment by ID. - * - * @param shipmentId The shipment ID - * @return The shipment - */ - @GET - @Path("/{shipmentId}") - @Operation(summary = "Get a shipment by ID") - @APIResponse(responseCode = "200", description = "Shipment found", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response getShipment( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId) { - - LOGGER.info("REST request to get shipment: " + shipmentId); - - Optional shipment = shipmentService.getShipment(shipmentId); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Gets all shipments. - * - * @return All shipments - */ - @GET - @Operation(summary = "Get all shipments") - @APIResponse(responseCode = "200", description = "All shipments", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getAllShipments() { - LOGGER.info("REST request to get all shipments"); - - List shipments = shipmentService.getAllShipments(); - return Response.ok(shipments).build(); - } - - /** - * Gets shipments by status. - * - * @param status The status - * @return The shipments with the given status - */ - @GET - @Path("/status/{status}") - @Operation(summary = "Get shipments by status") - @APIResponse(responseCode = "200", description = "Shipments with the given status", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getShipmentsByStatus( - @Parameter(description = "Shipment status", required = true) - @PathParam("status") ShipmentStatus status) { - - LOGGER.info("REST request to get shipments with status: " + status); - - List shipments = shipmentService.getShipmentsByStatus(status); - return Response.ok(shipments).build(); - } - - /** - * Gets shipments by order ID. - * - * @param orderId The order ID - * @return The shipments for the given order - */ - @GET - @Path("/orders/{orderId}") - @Operation(summary = "Get shipments by order ID") - @APIResponse(responseCode = "200", description = "Shipments for the given order", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getShipmentsByOrder( - @Parameter(description = "Order ID", required = true) - @PathParam("orderId") Long orderId) { - - LOGGER.info("REST request to get shipments for order: " + orderId); - - List shipments = shipmentService.getShipmentsByOrder(orderId); - return Response.ok(shipments).build(); - } - - /** - * Gets a shipment by tracking number. - * - * @param trackingNumber The tracking number - * @return The shipment - */ - @GET - @Path("/tracking/{trackingNumber}") - @Operation(summary = "Get a shipment by tracking number") - @APIResponse(responseCode = "200", description = "Shipment found", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response getShipmentByTrackingNumber( - @Parameter(description = "Tracking number", required = true) - @PathParam("trackingNumber") String trackingNumber) { - - LOGGER.info("REST request to get shipment with tracking number: " + trackingNumber); - - Optional shipment = shipmentService.getShipmentByTrackingNumber(trackingNumber); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the status of a shipment. - * - * @param shipmentId The shipment ID - * @param status The new status - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/status/{status}") - @Operation(summary = "Update shipment status") - @APIResponse(responseCode = "200", description = "Shipment status updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateShipmentStatus( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New status", required = true) - @PathParam("status") ShipmentStatus status) { - - LOGGER.info("REST request to update shipment " + shipmentId + " status to " + status); - - Optional shipment = shipmentService.updateShipmentStatus(shipmentId, status); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the carrier for a shipment. - * - * @param shipmentId The shipment ID - * @param carrier The new carrier - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/carrier") - @Operation(summary = "Update shipment carrier") - @APIResponse(responseCode = "200", description = "Carrier updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateCarrier( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New carrier", required = true) - @NotNull String carrier) { - - LOGGER.info("REST request to update carrier for shipment " + shipmentId + " to " + carrier); - - Optional shipment = shipmentService.updateCarrier(shipmentId, carrier); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the tracking number for a shipment. - * - * @param shipmentId The shipment ID - * @param trackingNumber The new tracking number - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/tracking") - @Operation(summary = "Update shipment tracking number") - @APIResponse(responseCode = "200", description = "Tracking number updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateTrackingNumber( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New tracking number", required = true) - @NotNull String trackingNumber) { - - LOGGER.info("REST request to update tracking number for shipment " + shipmentId + " to " + trackingNumber); - - Optional shipment = shipmentService.updateTrackingNumber(shipmentId, trackingNumber); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the estimated delivery date for a shipment. - * - * @param shipmentId The shipment ID - * @param dateStr The new estimated delivery date (ISO format) - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/delivery-date") - @Operation(summary = "Update shipment estimated delivery date") - @APIResponse(responseCode = "200", description = "Estimated delivery date updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "400", description = "Invalid date format") - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateEstimatedDelivery( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New estimated delivery date (ISO format: yyyy-MM-dd'T'HH:mm:ss)", required = true) - @NotNull String dateStr) { - - LOGGER.info("REST request to update estimated delivery for shipment " + shipmentId + " to " + dateStr); - - try { - LocalDateTime date = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); - Optional shipment = shipmentService.updateEstimatedDelivery(shipmentId, date); - - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } catch (Exception e) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Invalid date format. Use ISO format: yyyy-MM-dd'T'HH:mm:ss\"}") - .build(); - } - } - - /** - * Updates the notes for a shipment. - * - * @param shipmentId The shipment ID - * @param notes The new notes - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/notes") - @Operation(summary = "Update shipment notes") - @APIResponse(responseCode = "200", description = "Notes updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateNotes( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New notes", required = true) - String notes) { - - LOGGER.info("REST request to update notes for shipment " + shipmentId); - - Optional shipment = shipmentService.updateNotes(shipmentId, notes); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Deletes a shipment. - * - * @param shipmentId The shipment ID - * @return A response indicating success or failure - */ - @DELETE - @Path("/{shipmentId}") - @Operation(summary = "Delete a shipment") - @APIResponse(responseCode = "204", description = "Shipment deleted") - @APIResponse(responseCode = "404", description = "Shipment not found") - @APIResponse(responseCode = "400", description = "Shipment cannot be deleted due to its status") - public Response deleteShipment( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId) { - - LOGGER.info("REST request to delete shipment: " + shipmentId); - - boolean deleted = shipmentService.deleteShipment(shipmentId); - if (deleted) { - return Response.noContent().build(); - } - - // Check if shipment exists but cannot be deleted due to its status - Optional shipment = shipmentService.getShipment(shipmentId); - if (shipment.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Shipment cannot be deleted due to its status\"}") - .build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } -} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java deleted file mode 100644 index f29aadee..00000000 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java +++ /dev/null @@ -1,305 +0,0 @@ -package io.microprofile.tutorial.store.shipment.service; - -import io.microprofile.tutorial.store.shipment.client.OrderClient; -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; -import io.microprofile.tutorial.store.shipment.repository.ShipmentRepository; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.metrics.annotation.Counted; -import org.eclipse.microprofile.metrics.annotation.Timed; - -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.logging.Logger; - -/** - * Shipment Service for managing shipments. - */ -@ApplicationScoped -public class ShipmentService { - - private static final Logger LOGGER = Logger.getLogger(ShipmentService.class.getName()); - private static final Random RANDOM = new Random(); - private static final String[] CARRIERS = {"FedEx", "UPS", "USPS", "DHL", "Amazon Logistics"}; - - @Inject - private ShipmentRepository shipmentRepository; - - @Inject - private OrderClient orderClient; - - /** - * Creates a new shipment for an order. - * - * @param orderId The order ID - * @return The created shipment, or null if the order is invalid - */ - @Counted(name = "shipmentCreations", description = "Number of shipments created") - @Timed(name = "createShipmentTimer", description = "Time to create a shipment") - public Shipment createShipment(Long orderId) { - LOGGER.info("Creating shipment for order: " + orderId); - - // Verify that the order exists and is ready for shipment - if (!orderClient.verifyOrder(orderId)) { - LOGGER.warning("Order " + orderId + " is not valid for shipment"); - return null; - } - - // Get shipping address from order service - String shippingAddress = orderClient.getShippingAddress(orderId); - if (shippingAddress == null) { - LOGGER.warning("Could not retrieve shipping address for order " + orderId); - return null; - } - - // Create a new shipment - Shipment shipment = Shipment.builder() - .orderId(orderId) - .status(ShipmentStatus.PENDING) - .trackingNumber(generateTrackingNumber()) - .carrier(selectRandomCarrier()) - .shippingAddress(shippingAddress) - .estimatedDelivery(LocalDateTime.now().plusDays(5)) - .createdAt(LocalDateTime.now()) - .build(); - - Shipment savedShipment = shipmentRepository.save(shipment); - - // Update order status to indicate shipment is being processed - orderClient.updateOrderStatus(orderId, "SHIPMENT_CREATED"); - - return savedShipment; - } - - /** - * Updates the status of a shipment. - * - * @param shipmentId The shipment ID - * @param status The new status - * @return The updated shipment, or empty if not found - */ - @Counted(name = "shipmentStatusUpdates", description = "Number of shipment status updates") - public Optional updateShipmentStatus(Long shipmentId, ShipmentStatus status) { - LOGGER.info("Updating shipment " + shipmentId + " status to " + status); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setStatus(status); - shipment.setUpdatedAt(LocalDateTime.now()); - - // If status is SHIPPED, set the shipped date - if (status == ShipmentStatus.SHIPPED) { - shipment.setShippedAt(LocalDateTime.now()); - orderClient.updateOrderStatus(shipment.getOrderId(), "SHIPPED"); - } - // If status is DELIVERED, update order status - else if (status == ShipmentStatus.DELIVERED) { - orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERED"); - } - // If status is FAILED, update order status - else if (status == ShipmentStatus.FAILED) { - orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERY_FAILED"); - } - - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Gets a shipment by ID. - * - * @param shipmentId The shipment ID - * @return The shipment, or empty if not found - */ - public Optional getShipment(Long shipmentId) { - LOGGER.info("Getting shipment: " + shipmentId); - return shipmentRepository.findById(shipmentId); - } - - /** - * Gets all shipments for an order. - * - * @param orderId The order ID - * @return The list of shipments for the order - */ - public List getShipmentsByOrder(Long orderId) { - LOGGER.info("Getting shipments for order: " + orderId); - return shipmentRepository.findByOrderId(orderId); - } - - /** - * Gets a shipment by tracking number. - * - * @param trackingNumber The tracking number - * @return The shipment, or empty if not found - */ - public Optional getShipmentByTrackingNumber(String trackingNumber) { - LOGGER.info("Getting shipment with tracking number: " + trackingNumber); - List shipments = shipmentRepository.findByTrackingNumber(trackingNumber); - return shipments.isEmpty() ? Optional.empty() : Optional.of(shipments.get(0)); - } - - /** - * Gets all shipments. - * - * @return All shipments - */ - public List getAllShipments() { - LOGGER.info("Getting all shipments"); - return shipmentRepository.findAll(); - } - - /** - * Gets shipments by status. - * - * @param status The status - * @return The list of shipments with the given status - */ - public List getShipmentsByStatus(ShipmentStatus status) { - LOGGER.info("Getting shipments with status: " + status); - return shipmentRepository.findByStatus(status); - } - - /** - * Gets shipments due for delivery by the given date. - * - * @param date The date - * @return The list of shipments due by the given date - */ - public List getShipmentsDueBy(LocalDateTime date) { - LOGGER.info("Getting shipments due by: " + date); - return shipmentRepository.findByEstimatedDeliveryBefore(date); - } - - /** - * Updates the carrier for a shipment. - * - * @param shipmentId The shipment ID - * @param carrier The new carrier - * @return The updated shipment, or empty if not found - */ - public Optional updateCarrier(Long shipmentId, String carrier) { - LOGGER.info("Updating carrier for shipment " + shipmentId + " to " + carrier); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setCarrier(carrier); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the tracking number for a shipment. - * - * @param shipmentId The shipment ID - * @param trackingNumber The new tracking number - * @return The updated shipment, or empty if not found - */ - public Optional updateTrackingNumber(Long shipmentId, String trackingNumber) { - LOGGER.info("Updating tracking number for shipment " + shipmentId + " to " + trackingNumber); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setTrackingNumber(trackingNumber); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the estimated delivery date for a shipment. - * - * @param shipmentId The shipment ID - * @param estimatedDelivery The new estimated delivery date - * @return The updated shipment, or empty if not found - */ - public Optional updateEstimatedDelivery(Long shipmentId, LocalDateTime estimatedDelivery) { - LOGGER.info("Updating estimated delivery for shipment " + shipmentId + " to " + estimatedDelivery); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setEstimatedDelivery(estimatedDelivery); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the notes for a shipment. - * - * @param shipmentId The shipment ID - * @param notes The new notes - * @return The updated shipment, or empty if not found - */ - public Optional updateNotes(Long shipmentId, String notes) { - LOGGER.info("Updating notes for shipment " + shipmentId); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setNotes(notes); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Deletes a shipment. - * - * @param shipmentId The shipment ID - * @return true if the shipment was deleted, false if not found - */ - public boolean deleteShipment(Long shipmentId) { - LOGGER.info("Deleting shipment: " + shipmentId); - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - // Only allow deletion if the shipment is in PENDING or PROCESSING status - ShipmentStatus status = shipmentOpt.get().getStatus(); - if (status == ShipmentStatus.PENDING || status == ShipmentStatus.PROCESSING) { - return shipmentRepository.deleteById(shipmentId); - } - LOGGER.warning("Cannot delete shipment with status: " + status); - return false; - } - return false; - } - - /** - * Generates a random tracking number. - * - * @return A random tracking number - */ - private String generateTrackingNumber() { - return String.format("%s-%04d-%04d-%04d", - CARRIERS[RANDOM.nextInt(CARRIERS.length)].substring(0, 2).toUpperCase(), - RANDOM.nextInt(10000), - RANDOM.nextInt(10000), - RANDOM.nextInt(10000)); - } - - /** - * Selects a random carrier. - * - * @return A random carrier - */ - private String selectRandomCarrier() { - return CARRIERS[RANDOM.nextInt(CARRIERS.length)]; - } -} diff --git a/code/chapter11/shipment/src/main/liberty/config/server.xml b/code/chapter11/shipment/src/main/liberty/config/server.xml deleted file mode 100644 index 1f8cd2b0..00000000 --- a/code/chapter11/shipment/src/main/liberty/config/server.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - servlet-6.0 - pages-3.1 - microProfile-6.1 - - - - - - \ No newline at end of file diff --git a/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index 5057c129..00000000 --- a/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,32 +0,0 @@ -# Shipment Service Configuration - -# Order Service URL -order.service.url=http://localhost:8050/order - -# Configure health check properties -mp.health.check.timeout=5s - -# Configure default MP Metrics properties -mp.metrics.tags=app=shipment-service - -# Configure fault tolerance policies -# Retry configuration -mp.fault.tolerance.Retry.delay=1000 -mp.fault.tolerance.Retry.maxRetries=3 -mp.fault.tolerance.Retry.jitter=200 - -# Timeout configuration -mp.fault.tolerance.Timeout.value=5000 - -# Circuit Breaker configuration -mp.fault.tolerance.CircuitBreaker.requestVolumeThreshold=5 -mp.fault.tolerance.CircuitBreaker.failureRatio=0.5 -mp.fault.tolerance.CircuitBreaker.delay=10000 -mp.fault.tolerance.CircuitBreaker.successThreshold=2 - -# Open API configuration -mp.openapi.scan.disable=false -mp.openapi.scan.packages=io.microprofile.tutorial.store.shipment - -# In Docker environment, override the Order service URL -%docker.order.service.url=http://order:8050/order diff --git a/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index ed5b091b..00000000 --- a/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - Shipment Service - - - index.html - - - diff --git a/code/chapter11/shipment/src/main/webapp/index.html b/code/chapter11/shipment/src/main/webapp/index.html deleted file mode 100644 index 5641acb7..00000000 --- a/code/chapter11/shipment/src/main/webapp/index.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - Shipment Service - MicroProfile Tutorial - - - -

Shipment Service

-

- This is the Shipment Service for the MicroProfile Tutorial e-commerce application. - The service manages shipments for orders in the system. -

- -

REST API

-

- The service exposes the following endpoints: -

- -
-

POST /api/shipments/orders/{orderId}

-

Create a new shipment for an order.

-
- -
-

GET /api/shipments/{shipmentId}

-

Get a shipment by ID.

-
- -
-

GET /api/shipments

-

Get all shipments.

-
- -
-

GET /api/shipments/status/{status}

-

Get shipments by status (e.g., PENDING, PROCESSING, SHIPPED, etc.).

-
- -
-

GET /api/shipments/orders/{orderId}

-

Get all shipments for an order.

-
- -
-

GET /api/shipments/tracking/{trackingNumber}

-

Get a shipment by tracking number.

-
- -
-

PUT /api/shipments/{shipmentId}/status/{status}

-

Update the status of a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/carrier

-

Update the carrier for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/tracking

-

Update the tracking number for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/delivery-date

-

Update the estimated delivery date for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/notes

-

Update the notes for a shipment.

-
- -
-

DELETE /api/shipments/{shipmentId}

-

Delete a shipment (only allowed for shipments in PENDING or PROCESSING status).

-
- -

OpenAPI Documentation

-

- The service provides OpenAPI documentation at /shipment/openapi. - You can also access the Swagger UI at /shipment/openapi/ui. -

- -

Health Checks

-

- MicroProfile Health endpoints are available at: -

- - -

Metrics

-

- MicroProfile Metrics are available at /shipment/metrics. -

- -
-

Shipment Service - MicroProfile Tutorial E-commerce Application

-
- - diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java deleted file mode 100644 index e13684cc..00000000 --- a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Catalog Service. - */ -@ApplicationScoped -public class CatalogClient { - - private static final Logger LOGGER = Logger.getLogger(CatalogClient.class.getName()); - - @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog") - private String catalogServiceUrl; - - // Cache for product details to reduce service calls - private final Map productCache = new HashMap<>(); - - /** - * Gets product information from the catalog service. - * - * @param productId The product ID - * @return ProductInfo containing product details - */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "getProductInfoFallback") - public ProductInfo getProductInfo(Long productId) { - // Check cache first - if (productCache.containsKey(productId)) { - return productCache.get(productId); - } - - LOGGER.info(String.format("Fetching product info for product %d", productId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/products/%d", catalogServiceUrl, productId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple parsing - in a real app, use proper JSON parsing - String name = extractField(jsonResponse, "name"); - String priceStr = extractField(jsonResponse, "price"); - - double price = 0.0; - try { - price = Double.parseDouble(priceStr); - } catch (NumberFormatException e) { - LOGGER.warning("Failed to parse product price: " + priceStr); - } - - ProductInfo productInfo = new ProductInfo(productId, name, price); - - // Cache the result - productCache.put(productId, productInfo); - - return productInfo; - } - - LOGGER.warning(String.format("Failed to get product info. Status code: %d", response.getStatus())); - return new ProductInfo(productId, "Unknown Product", 0.0); - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Catalog Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for getProductInfo. - * Returns a placeholder product info when the catalog service is unavailable. - * - * @param productId The product ID - * @return A placeholder ProductInfo object - */ - public ProductInfo getProductInfoFallback(Long productId) { - LOGGER.warning(String.format("Using fallback for product info. Product ID: %d", productId)); - - // Check if we have a cached version - if (productCache.containsKey(productId)) { - return productCache.get(productId); - } - - // Return a placeholder - return new ProductInfo( - productId, - "Product " + productId + " (Service Unavailable)", - 0.0 - ); - } - - /** - * Helper method to extract field values from JSON string. - * This is a simplified approach - in a real app, use a proper JSON parser. - * - * @param jsonString The JSON string - * @param fieldName The name of the field to extract - * @return The extracted field value - */ - private String extractField(String jsonString, String fieldName) { - String searchPattern = "\"" + fieldName + "\":"; - if (jsonString.contains(searchPattern)) { - int startIndex = jsonString.indexOf(searchPattern) + searchPattern.length(); - int endIndex; - - // Skip whitespace - while (startIndex < jsonString.length() && - (jsonString.charAt(startIndex) == ' ' || jsonString.charAt(startIndex) == '\t')) { - startIndex++; - } - - if (startIndex < jsonString.length() && jsonString.charAt(startIndex) == '"') { - // String value - startIndex++; // Skip opening quote - endIndex = jsonString.indexOf("\"", startIndex); - } else { - // Number or boolean value - endIndex = jsonString.indexOf(",", startIndex); - if (endIndex == -1) { - endIndex = jsonString.indexOf("}", startIndex); - } - } - - if (endIndex > startIndex) { - return jsonString.substring(startIndex, endIndex); - } - } - return ""; - } - - /** - * Inner class to hold product information. - */ - public static class ProductInfo { - private final Long productId; - private final String name; - private final double price; - - public ProductInfo(Long productId, String name, double price) { - this.productId = productId; - this.name = name; - this.price = price; - } - - public Long getProductId() { - return productId; - } - - public String getName() { - return name; - } - - public double getPrice() { - return price; - } - } -} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java deleted file mode 100644 index b9ac4c04..00000000 --- a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Inventory Service. - */ -@ApplicationScoped -public class InventoryClient { - - private static final Logger LOGGER = Logger.getLogger(InventoryClient.class.getName()); - - @ConfigProperty(name = "inventory.service.url", defaultValue = "http://localhost:7050/inventory") - private String inventoryServiceUrl; - - /** - * Checks if a product is available in sufficient quantity. - * - * @param productId The product ID - * @param quantity The requested quantity - * @return true if the product is available in the requested quantity, false otherwise - */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "checkProductAvailabilityFallback") - public boolean checkProductAvailability(Long productId, int quantity) { - LOGGER.info(String.format("Checking availability for product %d, quantity %d", productId, quantity)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/inventories/product/%d", inventoryServiceUrl, productId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple parsing - in a real app, use proper JSON parsing - if (jsonResponse.contains("\"quantity\":")) { - String quantityStr = jsonResponse.split("\"quantity\":")[1].split(",")[0].trim(); - int availableQuantity = Integer.parseInt(quantityStr); - return availableQuantity >= quantity; - } - } - - LOGGER.warning(String.format("Failed to check product availability. Status code: %d", response.getStatus())); - return false; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Inventory Service", e); - throw e; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error parsing inventory response", e); - return false; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for checkProductAvailability. - * Always returns true to allow the cart operation to continue, - * but logs a warning. - * - * @param productId The product ID - * @param quantity The requested quantity - * @return true, allowing the operation to proceed - */ - public boolean checkProductAvailabilityFallback(Long productId, int quantity) { - LOGGER.warning(String.format( - "Using fallback for product availability check. Product ID: %d, Quantity: %d", - productId, quantity)); - // In a production system, you might want to cache product availability - // or implement a more sophisticated fallback mechanism - return true; // Allow the operation to proceed - } -} diff --git a/code/chapter11/shoppingcart/src/main/liberty/config/server.xml b/code/chapter11/shoppingcart/src/main/liberty/config/server.xml deleted file mode 100644 index 6f19cdff..00000000 --- a/code/chapter11/shoppingcart/src/main/liberty/config/server.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 - - - - - - \ No newline at end of file diff --git a/code/chapter11/user/README.adoc b/code/chapter11/user/README.adoc deleted file mode 100644 index fdcc577f..00000000 --- a/code/chapter11/user/README.adoc +++ /dev/null @@ -1,280 +0,0 @@ -= User Management Service -:toc: left -:icons: font -:source-highlighter: highlightjs -:sectnums: -:imagesdir: images - -This document provides information about the User Management Service, part of the MicroProfile tutorial store application. - -== Overview - -The User Management Service is responsible for user operations including: - -* User registration and management -* User profile information -* Basic authentication - -This service demonstrates MicroProfile and Jakarta EE technologies in a microservice architecture. - -== Technology Stack - -The User Management Service uses the following technologies: - -* Jakarta EE 10 -** RESTful Web Services (JAX-RS 3.1) -** Context and Dependency Injection (CDI 4.0) -** Bean Validation 3.0 -** JSON-B 3.0 -* MicroProfile 6.1 -** OpenAPI 3.1 -* Open Liberty -* Maven - -== Project Structure - -[source] ----- -user/ -├── src/ -│ ├── main/ -│ │ ├── java/ -│ │ │ └── io/microprofile/tutorial/store/user/ -│ │ │ ├── entity/ # Domain objects -│ │ │ ├── exception/ # Custom exceptions -│ │ │ ├── repository/ # Data access layer -│ │ │ ├── resource/ # REST endpoints -│ │ │ ├── service/ # Business logic -│ │ │ └── UserApplication.java -│ │ ├── liberty/ -│ │ │ └── config/ -│ │ │ └── server.xml # Liberty server configuration -│ │ ├── resources/ -│ │ │ └── META-INF/ -│ │ │ └── microprofile-config.properties -│ │ └── webapp/ -│ │ └── index.html # Welcome page -│ └── test/ # Unit and integration tests -└── pom.xml # Maven configuration ----- - -== API Endpoints - -The service exposes the following RESTful endpoints: - -[cols="2,1,4", options="header"] -|=== -| Endpoint | Method | Description - -| `/api/users` | GET | Retrieve all users -| `/api/users/{id}` | GET | Retrieve a specific user by ID -| `/api/users` | POST | Create a new user -| `/api/users/{id}` | PUT | Update an existing user -| `/api/users/{id}` | DELETE | Delete a user -|=== - -== Running the Service - -=== Prerequisites - -* JDK 17 or later -* Maven 3.8+ -* Docker (optional, for containerized deployment) - -=== Local Development - -1. Clone the repository: -+ -[source,bash] ----- -git clone https://github.com/your-org/liberty-rest-app.git -cd liberty-rest-app/user ----- - -2. Build the project: -+ -[source,bash] ----- -mvn clean package ----- - -3. Run the service: -+ -[source,bash] ----- -mvn liberty:run ----- - -4. The service will be available at: -+ -[source] ----- -http://localhost:6050/user/api/users ----- - -=== Docker Deployment - -To build and run using Docker: - -[source,bash] ----- -# Build the Docker image -docker build -t microprofile-tutorial/user-service . - -# Run the container -docker run -p 6050:6050 microprofile-tutorial/user-service ----- - -== Configuration - -The service can be configured using Liberty server.xml and MicroProfile Config: - -=== server.xml - -The main configuration file at `src/main/liberty/config/server.xml` includes: - -* HTTP endpoint configuration (port 6050) -* Feature enablement -* Application context configuration - -=== MicroProfile Config - -Environment-specific configuration can be modified in: -`src/main/resources/META-INF/microprofile-config.properties` - -== OpenAPI Documentation - -The service provides OpenAPI documentation of all endpoints. - -Access the OpenAPI UI at: -[source] ----- -http://localhost:6050/openapi/ui ----- - -Raw OpenAPI specification: -[source] ----- -http://localhost:6050/openapi ----- - -== Exception Handling - -The service includes a comprehensive exception handling strategy: - -* Custom exceptions for domain-specific errors -* Global exception mapping to appropriate HTTP status codes -* Consistent error response format - -Error responses follow this structure: - -[source,json] ----- -{ - "errorCode": "user_not_found", - "message": "User with ID 123 not found", - "timestamp": "2023-04-15T14:30:45Z" -} ----- - -Common error scenarios: - -* 400 Bad Request - Invalid input data -* 404 Not Found - Requested user doesn't exist -* 409 Conflict - Email address already in use - -== Testing - -=== Running Tests - -Execute unit and integration tests with: - -[source,bash] ----- -mvn test ----- - -=== Testing with cURL - -*Get all users:* -[source,bash] ----- -curl -X GET http://localhost:6050/user/api/users ----- - -*Get user by ID:* -[source,bash] ----- -curl -X GET http://localhost:6050/user/api/users/1 ----- - -*Create new user:* -[source,bash] ----- -curl -X POST http://localhost:6050/user/api/users \ - -H "Content-Type: application/json" \ - -d '{ - "name": "John Doe", - "email": "john@example.com", - "passwordHash": "hashed_password", - "address": "123 Main St", - "phone": "+1234567890" - }' ----- - -*Update user:* -[source,bash] ----- -curl -X PUT http://localhost:6050/user/api/users/1 \ - -H "Content-Type: application/json" \ - -d '{ - "name": "John Updated", - "email": "john@example.com", - "passwordHash": "hashed_password", - "address": "456 New Address", - "phone": "+1234567890" - }' ----- - -*Delete user:* -[source,bash] ----- -curl -X DELETE http://localhost:6050/user/api/users/1 ----- - -== Implementation Notes - -=== In-Memory Storage - -The service currently uses thread-safe in-memory storage: - -* `ConcurrentHashMap` for storing user data -* `AtomicLong` for generating sequence IDs -* No persistence to external databases - -For production use, consider implementing a proper database persistence layer. - -=== Security Considerations - -* Passwords are stored as hashes (not encrypted or in plain text) -* Input validation helps prevent injection attacks -* No authentication mechanism is implemented (for demo purposes only) - -== Troubleshooting - -=== Common Issues - -* *Port conflicts:* Check if port 6050 is already in use -* *CORS issues:* For browser access, check CORS configuration in server.xml -* *404 errors:* Verify the application context root and API path - -=== Logs - -* Liberty server logs are in `target/liberty/wlp/usr/servers/defaultServer/logs/` -* Application logs use standard JDK logging with info level by default - -== Further Resources - -* https://jakarta.ee/specifications/restful-ws/3.1/jakarta-restful-ws-spec-3.1.html[Jakarta RESTful Web Services Specification] -* https://openliberty.io/docs/latest/documentation.html[Open Liberty Documentation] -* https://download.eclipse.org/microprofile/microprofile-6.1/microprofile-spec-6.1.html[MicroProfile 6.1 Specification] \ No newline at end of file diff --git a/code/chapter11/user/pom.xml b/code/chapter11/user/pom.xml deleted file mode 100644 index f743ec40..00000000 --- a/code/chapter11/user/pom.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - 4.0.0 - - io.microprofile - user - 1.0-SNAPSHOT - war - - user-management - https://microprofile.io - - - UTF-8 - 17 - 17 - 10.0.0 - 6.1 - 23.0.0.3 - 1.18.24 - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - user - - - - io.openliberty.tools - liberty-maven-plugin - 3.8.2 - - userServer - runnable - 120 - - /user - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java deleted file mode 100644 index 347a04d2..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.microprofile.tutorial.store.user; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -/** - * Application class to activate REST resources. - */ -@ApplicationPath("/api") -public class UserApplication extends Application { - // The resources will be automatically discovered -} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java deleted file mode 100644 index c2fe3df0..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.microprofile.tutorial.store.user.entity; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * User entity for the microprofile tutorial store application. - * Represents a user in the system with their profile information. - * Uses in-memory storage with thread-safe operations. - * - * Key features: - * - Validated user information - * - Secure password storage (hashed) - * - Contact information validation - * - * Potential improvements: - * 1. Auditing fields: - * - createdAt: Timestamp for account creation - * - modifiedAt: Last modification timestamp - * - version: For optimistic locking in concurrent updates - * - * 2. Security enhancements: - * - passwordSalt: For more secure password hashing - * - lastPasswordChange: Track password updates - * - failedLoginAttempts: For account security - * - accountLocked: Boolean for account status - * - lockTimeout: Timestamp for temporary locks - * - * 3. Additional features: - * - userRole: ENUM for role-based access (USER, ADMIN, etc.) - * - status: ENUM for account state (ACTIVE, INACTIVE, SUSPENDED) - * - emailVerified: Boolean for email verification - * - timeZone: User's preferred timezone - * - locale: User's preferred language/region - * - lastLoginAt: Track user activity - * - * 4. Compliance: - * - privacyPolicyAccepted: Track user consent - * - marketingPreferences: User communication preferences - * - dataRetentionPolicy: For GDPR compliance - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class User { - - private Long userId; - - @NotEmpty(message = "Name cannot be empty") - @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") - private String name; - - @NotEmpty(message = "Email cannot be empty") - @Email(message = "Email should be valid") - @Size(max = 255, message = "Email must not exceed 255 characters") - private String email; - - @NotEmpty(message = "Password hash cannot be empty") - private String passwordHash; - - @Size(max = 200, message = "Address must not exceed 200 characters") - private String address; - - @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Phone number must be in E.164 format") - @Size(max = 15, message = "Phone number must not exceed 15 characters") - private String phoneNumber; -} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java deleted file mode 100644 index e240f3ab..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Entity classes for the user management module. - * - * This package contains the domain objects representing user data. - */ -package io.microprofile.tutorial.store.user.entity; diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java deleted file mode 100644 index a92fafcf..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * User management package for the microprofile tutorial store application. - * - * This package contains classes related to user management functionality. - */ -package io.microprofile.tutorial.store.user; \ No newline at end of file diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java deleted file mode 100644 index db979c0c..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.microprofile.tutorial.store.user.repository; - -import io.microprofile.tutorial.store.user.entity.User; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Thread-safe in-memory repository for User objects. - * This class provides CRUD operations for User entities using a ConcurrentHashMap for thread-safe storage - * and AtomicLong for safe ID generation in a concurrent environment. - * - * Key features: - * - Thread-safe operations using ConcurrentHashMap - * - Atomic ID generation - * - Immutable User objects in storage - * - Validation of user data - * - Optional return types for null-safety - * - * Note: This is a demo implementation. In production: - * - Consider using a persistent database - * - Add caching mechanisms - * - Implement proper pagination - * - Add audit logging - */ -@ApplicationScoped -public class UserRepository { - - private final Map users = new ConcurrentHashMap<>(); - private final AtomicLong nextId = new AtomicLong(1); - - /** - * Saves a user to the repository. - * If the user has no ID, a new ID is assigned. - * - * @param user The user to save - * @return The saved user with ID assigned - */ - public User save(User user) { - if (user.getUserId() == null) { - user.setUserId(nextId.getAndIncrement()); - } - User savedUser = User.builder() - .userId(user.getUserId()) - .name(user.getName()) - .email(user.getEmail()) - .passwordHash(user.getPasswordHash()) - .address(user.getAddress()) - .phoneNumber(user.getPhoneNumber()) - .build(); - users.put(savedUser.getUserId(), savedUser); - return savedUser; - } - - /** - * Finds a user by ID. - * - * @param id The user ID - * @return An Optional containing the user if found, or empty if not found - */ - public Optional findById(Long id) { - return Optional.ofNullable(users.get(id)); - } - - /** - * Finds a user by email. - * - * @param email The user's email - * @return An Optional containing the user if found, or empty if not found - */ - public Optional findByEmail(String email) { - return users.values().stream() - .filter(user -> user.getEmail().equals(email)) - .findFirst(); - } - - /** - * Retrieves all users from the repository. - * - * @return A list of all users - */ - public List findAll() { - return new ArrayList<>(users.values()); - } - - /** - * Deletes a user by ID. - * - * @param id The ID of the user to delete - * @return true if the user was deleted, false if not found - */ - public boolean deleteById(Long id) { - return users.remove(id) != null; - } - - /** - * Updates an existing user. - * - * @param id The ID of the user to update - * @param user The updated user information - * @return An Optional containing the updated user, or empty if not found - */ - /** - * Updates an existing user atomically. - * Only updates the user if it exists and the update is valid. - * - * @param id The ID of the user to update - * @param user The updated user information - * @return An Optional containing the updated user, or empty if not found - * @throws IllegalArgumentException if user is null or has invalid data - */ - public Optional update(Long id, User user) { - if (user == null) { - throw new IllegalArgumentException("User cannot be null"); - } - - return Optional.ofNullable(users.computeIfPresent(id, (key, existingUser) -> { - User updatedUser = User.builder() - .userId(id) - .name(user.getName() != null ? user.getName() : existingUser.getName()) - .email(user.getEmail() != null ? user.getEmail() : existingUser.getEmail()) - .passwordHash(user.getPasswordHash() != null ? user.getPasswordHash() : existingUser.getPasswordHash()) - .address(user.getAddress() != null ? user.getAddress() : existingUser.getAddress()) - .phoneNumber(user.getPhoneNumber() != null ? user.getPhoneNumber() : existingUser.getPhoneNumber()) - .build(); - return updatedUser; - })); - } -} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java deleted file mode 100644 index 0988dcb5..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Repository classes for the user management module. - * - * This package contains classes responsible for data access and persistence. - */ -package io.microprofile.tutorial.store.user.repository; diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java deleted file mode 100644 index bdd2e21b..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.microprofile.tutorial.store.user.resource; - -import io.microprofile.tutorial.store.user.entity.User; -import io.microprofile.tutorial.store.user.service.UserService; - -import java.net.URI; -import java.util.List; - -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * REST resource for user management operations. - * Provides endpoints for creating, retrieving, updating, and deleting users. - * Implements standard RESTful practices with proper status codes and hypermedia links. - */ -@Path("/users") -@Tag(name = "User Management", description = "Operations for managing users") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public class UserResource { - - @Inject - private UserService userService; - - @Context - private UriInfo uriInfo; - - @GET - @Operation(summary = "Get all users", description = "Returns a list of all users") - @APIResponse(responseCode = "200", description = "List of users") - @APIResponse(responseCode = "204", description = "No users found") - public Response getAllUsers() { - List users = userService.getAllUsers(); - - if (users.isEmpty()) { - return Response.noContent().build(); - } - - return Response.ok(users).build(); - } - - @GET - @Path("/{id}") - @Operation(summary = "Get user by ID", description = "Returns a user by their ID") - @APIResponse(responseCode = "200", description = "User found") - @APIResponse(responseCode = "404", description = "User not found") - public Response getUserById( - @PathParam("id") - @Parameter(description = "User ID", required = true) - Long id) { - User user = userService.getUserById(id); - // Add HATEOAS links - URI selfLink = uriInfo.getBaseUriBuilder() - .path(UserResource.class) - .path(String.valueOf(user.getUserId())) - .build(); - return Response.ok(user) - .link(selfLink, "self") - .build(); - } - - @POST - @Operation(summary = "Create new user", description = "Creates a new user") - @APIResponse(responseCode = "201", description = "User created successfully") - @APIResponse(responseCode = "400", description = "Invalid user data") - @APIResponse(responseCode = "409", description = "Email already in use") - public Response createUser( - @Valid - @NotNull(message = "Request body cannot be empty") - @Parameter(description = "User to create", required = true) - User user) { - User createdUser = userService.createUser(user); - URI location = uriInfo.getAbsolutePathBuilder() - .path(String.valueOf(createdUser.getUserId())) - .build(); - return Response.created(location) - .entity(createdUser) - .build(); - } - - @PUT - @Path("/{id}") - @Operation(summary = "Update user", description = "Updates an existing user") - @APIResponse(responseCode = "200", description = "User updated successfully") - @APIResponse(responseCode = "400", description = "Invalid user data") - @APIResponse(responseCode = "404", description = "User not found") - @APIResponse(responseCode = "409", description = "Email already in use") - public Response updateUser( - @PathParam("id") - @Parameter(description = "User ID", required = true) - Long id, - - @Valid - @NotNull(message = "Request body cannot be empty") - @Parameter(description = "Updated user information", required = true) - User user) { - User updatedUser = userService.updateUser(id, user); - return Response.ok(updatedUser).build(); - } - - @DELETE - @Path("/{id}") - @Operation(summary = "Delete user", description = "Deletes a user by ID") - @APIResponse(responseCode = "204", description = "User successfully deleted") - @APIResponse(responseCode = "404", description = "User not found") - public Response deleteUser( - @PathParam("id") - @Parameter(description = "User ID to delete", required = true) - Long id) { - userService.deleteUser(id); - return Response.noContent().build(); - } -} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java deleted file mode 100644 index e69de29b..00000000 diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java deleted file mode 100644 index db81d5e5..00000000 --- a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.microprofile.tutorial.store.user.service; - -import io.microprofile.tutorial.store.user.entity.User; -import io.microprofile.tutorial.store.user.repository.UserRepository; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.List; -import java.util.Optional; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response; - -/** - * Service class for User management operations. - */ -@ApplicationScoped -public class UserService { - - @Inject - private UserRepository userRepository; - - /** - * Creates a new user. - * - * @param user The user to create - * @return The created user - * @throws WebApplicationException if a user with the email already exists - */ - public User createUser(User user) { - // Check if email already exists - Optional existingUser = userRepository.findByEmail(user.getEmail()); - if (existingUser.isPresent()) { - throw new WebApplicationException("Email already in use", Response.Status.CONFLICT); - } - - // Hash the password - if (user.getPasswordHash() != null) { - user.setPasswordHash(hashPassword(user.getPasswordHash())); - } - - return userRepository.save(user); - } - - /** - * Gets a user by ID. - * - * @param id The user ID - * @return The user - * @throws WebApplicationException if the user is not found - */ - public User getUserById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new WebApplicationException("User not found", Response.Status.NOT_FOUND)); - } - - /** - * Gets all users. - * - * @return A list of all users - */ - public List getAllUsers() { - return userRepository.findAll(); - } - - /** - * Updates a user. - * - * @param id The user ID - * @param user The updated user information - * @return The updated user - * @throws WebApplicationException if the user is not found or if updating to an email that's already in use - */ - public User updateUser(Long id, User user) { - // Check if email already exists and belongs to another user - Optional existingUserWithEmail = userRepository.findByEmail(user.getEmail()); - if (existingUserWithEmail.isPresent() && !existingUserWithEmail.get().getUserId().equals(id)) { - throw new WebApplicationException("Email already in use", Response.Status.CONFLICT); - } - - // Hash the password if it has changed - if (user.getPasswordHash() != null && - !user.getPasswordHash().matches("^[a-fA-F0-9]{64}$")) { // Simple check if it's already a SHA-256 hash - user.setPasswordHash(hashPassword(user.getPasswordHash())); - } - - return userRepository.update(id, user) - .orElseThrow(() -> new WebApplicationException("User not found", Response.Status.NOT_FOUND)); - } - - /** - * Deletes a user. - * - * @param id The user ID - * @throws WebApplicationException if the user is not found - */ - public void deleteUser(Long id) { - boolean deleted = userRepository.deleteById(id); - if (!deleted) { - throw new WebApplicationException("User not found", Response.Status.NOT_FOUND); - } - } - - /** - * Simple password hashing using SHA-256. - * Note: In a production environment, use a more secure hashing algorithm with salt - * - * @param password The password to hash - * @return The hashed password - */ - private String hashPassword(String password) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(password.getBytes()); - - StringBuilder hexString = new StringBuilder(); - for (byte b : hash) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) hexString.append('0'); - hexString.append(hex); - } - - return hexString.toString(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Failed to hash password", e); - } - } -} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java deleted file mode 100644 index e69de29b..00000000 diff --git a/code/chapter11/user/src/main/liberty/config/server.xml b/code/chapter11/user/src/main/liberty/config/server.xml deleted file mode 100644 index 7fbd4773..00000000 --- a/code/chapter11/user/src/main/liberty/config/server.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 - - - - - - \ No newline at end of file diff --git a/code/chapter11/user/src/main/webapp/index.html b/code/chapter11/user/src/main/webapp/index.html deleted file mode 100644 index fdb15f44..00000000 --- a/code/chapter11/user/src/main/webapp/index.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - User Management Service API - - - -

User Management Service API

-

This service provides RESTful endpoints for managing users in the MicroProfile REST application.

- -

Available Endpoints

- -
-
GET /api/users
-
Get all users
-
- Response: 200 (List of users), 204 (No users found) -
-
- -
-
GET /api/users/{id}
-
Get a specific user by ID
-
- Response: 200 (User found), 404 (User not found) -
-
- -
-
POST /api/users
-
Create a new user
-
- Request Body: User JSON object
- Response: 201 (User created), 400 (Invalid data), 409 (Email already in use) -
-
- -
-
PUT /api/users/{id}
-
Update an existing user
-
- Request Body: Updated User JSON object
- Response: 200 (User updated), 400 (Invalid data), 404 (User not found), 409 (Email already in use) -
-
- -
-
DELETE /api/users/{id}
-
Delete a user by ID
-
- Response: 204 (User deleted), 404 (User not found) -
-
- -

Features

-
    -
  • Full CRUD operations for user management
  • -
  • Input validation using Bean Validation
  • -
  • HATEOAS links for improved API discoverability
  • -
  • OpenAPI documentation annotations
  • -
  • Proper HTTP status codes and error handling
  • -
  • Email uniqueness validation
  • -
- -

Example User JSON

-
-{
-    "userId": 1,
-    "email": "user@example.com",
-    "firstName": "John",
-    "lastName": "Doe"
-}
-    
- - From 45f1e70c2ae0b99806aeea5e357c12dd4f7e6062 Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 18:14:28 +0000 Subject: [PATCH 5/7] Delete files --- code/chapter11/chapter11/shipment/Dockerfile | 27 ++ code/chapter11/chapter11/shipment/README.md | 87 ++++ code/chapter11/chapter11/shipment/pom.xml | 114 +++++ .../chapter11/shipment/run-docker.sh | 11 + code/chapter11/chapter11/shipment/run.sh | 12 + .../store/shipment/ShipmentApplication.java | 35 ++ .../store/shipment/client/OrderClient.java | 193 +++++++++ .../store/shipment/entity/Shipment.java | 45 ++ .../store/shipment/entity/ShipmentStatus.java | 16 + .../store/shipment/filter/CorsFilter.java | 43 ++ .../shipment/health/ShipmentHealthCheck.java | 67 +++ .../repository/ShipmentRepository.java | 148 +++++++ .../shipment/resource/ShipmentResource.java | 397 ++++++++++++++++++ .../shipment/service/ShipmentService.java | 305 ++++++++++++++ .../META-INF/microprofile-config.properties | 32 ++ .../shipment/src/main/webapp/WEB-INF/web.xml | 13 + .../shipment/src/main/webapp/index.html | 150 +++++++ .../shoppingcart/client/CatalogClient.java | 184 ++++++++ .../shoppingcart/client/InventoryClient.java | 96 +++++ 19 files changed, 1975 insertions(+) create mode 100644 code/chapter11/chapter11/shipment/Dockerfile create mode 100644 code/chapter11/chapter11/shipment/README.md create mode 100644 code/chapter11/chapter11/shipment/pom.xml create mode 100644 code/chapter11/chapter11/shipment/run-docker.sh create mode 100644 code/chapter11/chapter11/shipment/run.sh create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java create mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java create mode 100644 code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties create mode 100644 code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/chapter11/shipment/src/main/webapp/index.html create mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java create mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java diff --git a/code/chapter11/chapter11/shipment/Dockerfile b/code/chapter11/chapter11/shipment/Dockerfile new file mode 100644 index 00000000..287b43d0 --- /dev/null +++ b/code/chapter11/chapter11/shipment/Dockerfile @@ -0,0 +1,27 @@ +FROM icr.io/appcafe/open-liberty:23.0.0.3-full-java17-openj9-ubi + +# Copy config +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Create the app directory +COPY --chown=1001:0 target/shipment.war /config/apps/ + +# Optional: Copy utility scripts +COPY --chown=1001:0 *.sh /opt/ol/helpers/ + +# Environment variables +ENV VERBOSE=true + +# This is important - adds the management of vulnerability databases to allow Docker scanning +RUN dnf install -y shadow-utils + +# Set environment variable for MP config profile +ENV MP_CONFIG_PROFILE=docker + +EXPOSE 8060 9060 + +# Run as non-root user for security +USER 1001 + +# Start Liberty +ENTRYPOINT ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/chapter11/shipment/README.md b/code/chapter11/chapter11/shipment/README.md new file mode 100644 index 00000000..41619949 --- /dev/null +++ b/code/chapter11/chapter11/shipment/README.md @@ -0,0 +1,87 @@ +# Shipment Service + +This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. + +## Overview + +The Shipment Service is responsible for: +- Creating shipments for orders +- Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) +- Assigning tracking numbers +- Estimating delivery dates +- Communicating with the Order Service to update order status + +## Technologies + +The Shipment Service is built using: +- Jakarta EE 10 +- MicroProfile 6.1 +- Open Liberty +- Java 17 + +## Getting Started + +### Prerequisites + +- JDK 17+ +- Maven 3.8+ +- Docker (for containerized deployment) + +### Running Locally + +To build and run the service: + +```bash +./run.sh +``` + +This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment + +### Running with Docker + +To build and run the service in a Docker container: + +```bash +./run-docker.sh +``` + +This will build a Docker image for the service and run it, exposing ports 8060 and 9060. + +## API Endpoints + +| Method | URL | Description | +|--------|-------------------------------------------|--------------------------------------| +| POST | /api/shipments/orders/{orderId} | Create a new shipment | +| GET | /api/shipments/{shipmentId} | Get a shipment by ID | +| GET | /api/shipments | Get all shipments | +| GET | /api/shipments/status/{status} | Get shipments by status | +| GET | /api/shipments/orders/{orderId} | Get shipments for an order | +| GET | /api/shipments/tracking/{trackingNumber} | Get a shipment by tracking number | +| PUT | /api/shipments/{shipmentId}/status/{status} | Update shipment status | +| PUT | /api/shipments/{shipmentId}/carrier | Update shipment carrier | +| PUT | /api/shipments/{shipmentId}/tracking | Update shipment tracking number | +| PUT | /api/shipments/{shipmentId}/delivery-date | Update estimated delivery date | +| PUT | /api/shipments/{shipmentId}/notes | Update shipment notes | +| DELETE | /api/shipments/{shipmentId} | Delete a shipment | + +## MicroProfile Features + +The service utilizes several MicroProfile features: + +- **Config**: For external configuration +- **Health**: For liveness and readiness checks +- **Metrics**: For monitoring service performance +- **Fault Tolerance**: For resilient communication with the Order Service +- **OpenAPI**: For API documentation + +## Documentation + +API documentation is available at: +- OpenAPI: http://localhost:8060/shipment/openapi +- Swagger UI: http://localhost:8060/shipment/openapi/ui + +## Monitoring + +Health and metrics endpoints: +- Health: http://localhost:8060/shipment/health +- Metrics: http://localhost:8060/shipment/metrics diff --git a/code/chapter11/chapter11/shipment/pom.xml b/code/chapter11/chapter11/shipment/pom.xml new file mode 100644 index 00000000..d5e31f65 --- /dev/null +++ b/code/chapter11/chapter11/shipment/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.microprofile + shipment + 1.0-SNAPSHOT + war + + shipment-service + https://microprofile.io + + + UTF-8 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + shipment + + + + io.openliberty.tools + liberty-maven-plugin + 3.8.2 + + shipmentServer + runnable + 120 + + /shipment + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/chapter11/shipment/run-docker.sh b/code/chapter11/chapter11/shipment/run-docker.sh new file mode 100644 index 00000000..69a51506 --- /dev/null +++ b/code/chapter11/chapter11/shipment/run-docker.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Build and run the Shipment Service in Docker +echo "Building and starting Shipment Service in Docker..." + +# Build the application +mvn clean package + +# Build and run the Docker image +docker build -t shipment-service . +docker run -p 8060:8060 -p 9060:9060 --name shipment-service shipment-service diff --git a/code/chapter11/chapter11/shipment/run.sh b/code/chapter11/chapter11/shipment/run.sh new file mode 100644 index 00000000..b6fd34ac --- /dev/null +++ b/code/chapter11/chapter11/shipment/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Build and run the Shipment Service +echo "Building and starting Shipment Service..." + +# Stop running server if already running +if [ -f target/liberty/wlp/usr/servers/shipmentServer/workarea/.sRunning ]; then + mvn liberty:stop +fi + +# Clean, build and run +mvn clean package liberty:run diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java new file mode 100644 index 00000000..3f7288ba --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java @@ -0,0 +1,35 @@ +package io.microprofile.tutorial.store.shipment; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.info.Contact; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.License; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * JAX-RS Application class for the shipment service. + */ +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Shipment Service API", + version = "1.0.0", + description = "API for managing shipments in the microprofile tutorial store", + contact = @Contact( + name = "Shipment Service Support", + email = "shipment@example.com" + ), + license = @License( + name = "Apache 2.0", + url = "https://www.apache.org/licenses/LICENSE-2.0.html" + ) + ), + tags = { + @Tag(name = "Shipment Resource", description = "Operations for managing shipments") + } +) +public class ShipmentApplication extends Application { + // Empty application class, all configuration is provided by annotations +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java new file mode 100644 index 00000000..ba3ce1f8 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java @@ -0,0 +1,193 @@ +package io.microprofile.tutorial.store.shipment.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Order Service. + */ +@ApplicationScoped +public class OrderClient { + + private static final Logger LOGGER = Logger.getLogger(OrderClient.class.getName()); + + @ConfigProperty(name = "order.service.url", defaultValue = "http://localhost:8050/order") + private String orderServiceUrl; + + /** + * Updates the order status after a shipment has been processed. + * + * @param orderId The ID of the order to update + * @param newStatus The new status for the order + * @return true if the update was successful, false otherwise + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "updateOrderStatusFallback") + public boolean updateOrderStatus(Long orderId, String newStatus) { + LOGGER.info(String.format("Updating order %d status to %s", orderId, newStatus)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d/status/%s", orderServiceUrl, orderId, newStatus); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .put(Entity.json("{}")); + + boolean success = response.getStatus() == Response.Status.OK.getStatusCode(); + if (!success) { + LOGGER.warning(String.format("Failed to update order status. Status code: %d", response.getStatus())); + } + return success; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Verifies that an order exists and is in a valid state for shipment. + * + * @param orderId The ID of the order to verify + * @return true if the order exists and is in a valid state, false otherwise + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "verifyOrderFallback") + public boolean verifyOrder(Long orderId) { + LOGGER.info(String.format("Verifying order %d for shipment", orderId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple check if the order is in a valid state for shipment + // In a real app, we'd parse the JSON properly + return jsonResponse.contains("\"status\":\"PAID\"") || + jsonResponse.contains("\"status\":\"PROCESSING\"") || + jsonResponse.contains("\"status\":\"READY_FOR_SHIPMENT\""); + } + + LOGGER.warning(String.format("Failed to verify order. Status code: %d", response.getStatus())); + return false; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Gets the shipping address for an order. + * + * @param orderId The ID of the order + * @return The shipping address, or null if not found + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "getShippingAddressFallback") + public String getShippingAddress(Long orderId) { + LOGGER.info(String.format("Getting shipping address for order %d", orderId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple extract of shipping address - in real app use proper JSON parsing + if (jsonResponse.contains("\"shippingAddress\":")) { + int startIndex = jsonResponse.indexOf("\"shippingAddress\":") + "\"shippingAddress\":".length(); + startIndex = jsonResponse.indexOf("\"", startIndex) + 1; + int endIndex = jsonResponse.indexOf("\"", startIndex); + if (endIndex > startIndex) { + return jsonResponse.substring(startIndex, endIndex); + } + } + } + + LOGGER.warning(String.format("Failed to get shipping address. Status code: %d", response.getStatus())); + return null; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for updateOrderStatus. + * + * @param orderId The ID of the order + * @param newStatus The new status for the order + * @return false, indicating failure + */ + public boolean updateOrderStatusFallback(Long orderId, String newStatus) { + LOGGER.warning(String.format("Using fallback for order status update. Order ID: %d, Status: %s", orderId, newStatus)); + return false; + } + + /** + * Fallback method for verifyOrder. + * + * @param orderId The ID of the order + * @return false, indicating failure + */ + public boolean verifyOrderFallback(Long orderId) { + LOGGER.warning(String.format("Using fallback for order verification. Order ID: %d", orderId)); + return false; + } + + /** + * Fallback method for getShippingAddress. + * + * @param orderId The ID of the order + * @return null, indicating failure + */ + public String getShippingAddressFallback(Long orderId) { + LOGGER.warning(String.format("Using fallback for getting shipping address. Order ID: %d", orderId)); + return null; + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java new file mode 100644 index 00000000..d9bea89e --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java @@ -0,0 +1,45 @@ +package io.microprofile.tutorial.store.shipment.entity; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Shipment class for the microprofile tutorial store application. + * This class represents a shipment of an order in the system. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Shipment { + + private Long shipmentId; + + @NotNull(message = "Order ID cannot be null") + private Long orderId; + + private String trackingNumber; + + @NotNull(message = "Status cannot be null") + private ShipmentStatus status; + + private LocalDateTime estimatedDelivery; + + private LocalDateTime shippedAt; + + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + private LocalDateTime updatedAt; + + private String carrier; + + private String shippingAddress; + + private String notes; +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java new file mode 100644 index 00000000..0e120a9b --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java @@ -0,0 +1,16 @@ +package io.microprofile.tutorial.store.shipment.entity; + +/** + * ShipmentStatus enum for the microprofile tutorial store application. + * This enum defines the possible statuses for a shipment. + */ +public enum ShipmentStatus { + PENDING, // Shipment is pending + PROCESSING, // Shipment is being processed + SHIPPED, // Shipment has been shipped + IN_TRANSIT, // Shipment is in transit + OUT_FOR_DELIVERY,// Shipment is out for delivery + DELIVERED, // Shipment has been delivered + FAILED, // Shipment delivery failed + RETURNED // Shipment was returned +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java new file mode 100644 index 00000000..ec26495e --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java @@ -0,0 +1,43 @@ +package io.microprofile.tutorial.store.shipment.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Filter to enable CORS for the Shipment service. + */ +public class CorsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // No initialization required + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Allow requests from any origin + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + response.setHeader("Access-Control-Max-Age", "3600"); + + // For preflight requests + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // No cleanup required + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java new file mode 100644 index 00000000..4bf8a503 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java @@ -0,0 +1,67 @@ +package io.microprofile.tutorial.store.shipment.health; + +import io.microprofile.tutorial.store.shipment.client.OrderClient; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.eclipse.microprofile.health.Readiness; + +/** + * Health check for the shipment service. + */ +@ApplicationScoped +public class ShipmentHealthCheck { + + @Inject + private OrderClient orderClient; + + /** + * Liveness check for the shipment service. + * Verifies that the application is running and not in a failed state. + * + * @return HealthCheckResponse indicating whether the service is live + */ + @Liveness + @ApplicationScoped + public static class LivenessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.named("shipment-liveness") + .up() + .withData("memory", Runtime.getRuntime().freeMemory()) + .build(); + } + } + + /** + * Readiness check for the shipment service. + * Verifies that the service is ready to handle requests, including connectivity to dependencies. + * + * @return HealthCheckResponse indicating whether the service is ready + */ + @Readiness + @ApplicationScoped + public class ReadinessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + boolean orderServiceReachable = false; + + try { + // Simple check to see if the Order service is reachable + // We use a dummy order ID just to test connectivity + orderClient.getShippingAddress(999999L); + orderServiceReachable = true; + } catch (Exception e) { + // If the order service is not reachable, the health check will fail + orderServiceReachable = false; + } + + return HealthCheckResponse.named("shipment-readiness") + .status(orderServiceReachable) + .withData("orderServiceReachable", orderServiceReachable) + .build(); + } + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java new file mode 100644 index 00000000..c4013a92 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java @@ -0,0 +1,148 @@ +package io.microprofile.tutorial.store.shipment.repository; + +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Simple in-memory repository for Shipment objects. + * This class provides CRUD operations for Shipment entities. + */ +@ApplicationScoped +public class ShipmentRepository { + + private final Map shipments = new ConcurrentHashMap<>(); + private long nextId = 1; + + /** + * Saves a shipment to the repository. + * If the shipment has no ID, a new ID is assigned. + * + * @param shipment The shipment to save + * @return The saved shipment with ID assigned + */ + public Shipment save(Shipment shipment) { + if (shipment.getShipmentId() == null) { + shipment.setShipmentId(nextId++); + } + + if (shipment.getCreatedAt() == null) { + shipment.setCreatedAt(LocalDateTime.now()); + } + + shipment.setUpdatedAt(LocalDateTime.now()); + + shipments.put(shipment.getShipmentId(), shipment); + return shipment; + } + + /** + * Finds a shipment by ID. + * + * @param id The shipment ID + * @return An Optional containing the shipment if found, or empty if not found + */ + public Optional findById(Long id) { + return Optional.ofNullable(shipments.get(id)); + } + + /** + * Finds shipments by order ID. + * + * @param orderId The order ID + * @return A list of shipments for the specified order + */ + public List findByOrderId(Long orderId) { + return shipments.values().stream() + .filter(shipment -> shipment.getOrderId().equals(orderId)) + .collect(Collectors.toList()); + } + + /** + * Finds shipments by tracking number. + * + * @param trackingNumber The tracking number + * @return A list of shipments with the specified tracking number + */ + public List findByTrackingNumber(String trackingNumber) { + return shipments.values().stream() + .filter(shipment -> trackingNumber.equals(shipment.getTrackingNumber())) + .collect(Collectors.toList()); + } + + /** + * Finds shipments by status. + * + * @param status The shipment status + * @return A list of shipments with the specified status + */ + public List findByStatus(ShipmentStatus status) { + return shipments.values().stream() + .filter(shipment -> shipment.getStatus() == status) + .collect(Collectors.toList()); + } + + /** + * Finds shipments that are expected to be delivered by a certain date. + * + * @param deliveryDate The delivery date + * @return A list of shipments expected to be delivered by the specified date + */ + public List findByEstimatedDeliveryBefore(LocalDateTime deliveryDate) { + return shipments.values().stream() + .filter(shipment -> shipment.getEstimatedDelivery() != null && + shipment.getEstimatedDelivery().isBefore(deliveryDate)) + .collect(Collectors.toList()); + } + + /** + * Retrieves all shipments from the repository. + * + * @return A list of all shipments + */ + public List findAll() { + return new ArrayList<>(shipments.values()); + } + + /** + * Deletes a shipment by ID. + * + * @param id The ID of the shipment to delete + * @return true if the shipment was deleted, false if not found + */ + public boolean deleteById(Long id) { + return shipments.remove(id) != null; + } + + /** + * Updates an existing shipment. + * + * @param id The ID of the shipment to update + * @param shipment The updated shipment information + * @return An Optional containing the updated shipment, or empty if not found + */ + public Optional update(Long id, Shipment shipment) { + if (!shipments.containsKey(id)) { + return Optional.empty(); + } + + // Preserve creation date + LocalDateTime createdAt = shipments.get(id).getCreatedAt(); + shipment.setCreatedAt(createdAt); + + shipment.setShipmentId(id); + shipment.setUpdatedAt(LocalDateTime.now()); + + shipments.put(id, shipment); + return Optional.of(shipment); + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java new file mode 100644 index 00000000..602be808 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java @@ -0,0 +1,397 @@ +package io.microprofile.tutorial.store.shipment.resource; + +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; +import io.microprofile.tutorial.store.shipment.service.ShipmentService; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * REST resource for shipment operations. + */ +@Path("/api/shipments") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Shipment Resource", description = "Operations for managing shipments") +public class ShipmentResource { + + private static final Logger LOGGER = Logger.getLogger(ShipmentResource.class.getName()); + + @Inject + private ShipmentService shipmentService; + + /** + * Creates a new shipment for an order. + * + * @param orderId The order ID + * @return The created shipment + */ + @POST + @Path("/orders/{orderId}") + @Operation(summary = "Create a new shipment for an order") + @APIResponse(responseCode = "201", description = "Shipment created", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "400", description = "Invalid order ID") + @APIResponse(responseCode = "404", description = "Order not found or not ready for shipment") + public Response createShipment( + @Parameter(description = "Order ID", required = true) + @PathParam("orderId") Long orderId) { + + LOGGER.info("REST request to create shipment for order: " + orderId); + + Shipment shipment = shipmentService.createShipment(orderId); + if (shipment == null) { + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Order not found or not ready for shipment\"}") + .build(); + } + + return Response.status(Response.Status.CREATED) + .entity(shipment) + .build(); + } + + /** + * Gets a shipment by ID. + * + * @param shipmentId The shipment ID + * @return The shipment + */ + @GET + @Path("/{shipmentId}") + @Operation(summary = "Get a shipment by ID") + @APIResponse(responseCode = "200", description = "Shipment found", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response getShipment( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId) { + + LOGGER.info("REST request to get shipment: " + shipmentId); + + Optional shipment = shipmentService.getShipment(shipmentId); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Gets all shipments. + * + * @return All shipments + */ + @GET + @Operation(summary = "Get all shipments") + @APIResponse(responseCode = "200", description = "All shipments", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getAllShipments() { + LOGGER.info("REST request to get all shipments"); + + List shipments = shipmentService.getAllShipments(); + return Response.ok(shipments).build(); + } + + /** + * Gets shipments by status. + * + * @param status The status + * @return The shipments with the given status + */ + @GET + @Path("/status/{status}") + @Operation(summary = "Get shipments by status") + @APIResponse(responseCode = "200", description = "Shipments with the given status", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getShipmentsByStatus( + @Parameter(description = "Shipment status", required = true) + @PathParam("status") ShipmentStatus status) { + + LOGGER.info("REST request to get shipments with status: " + status); + + List shipments = shipmentService.getShipmentsByStatus(status); + return Response.ok(shipments).build(); + } + + /** + * Gets shipments by order ID. + * + * @param orderId The order ID + * @return The shipments for the given order + */ + @GET + @Path("/orders/{orderId}") + @Operation(summary = "Get shipments by order ID") + @APIResponse(responseCode = "200", description = "Shipments for the given order", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getShipmentsByOrder( + @Parameter(description = "Order ID", required = true) + @PathParam("orderId") Long orderId) { + + LOGGER.info("REST request to get shipments for order: " + orderId); + + List shipments = shipmentService.getShipmentsByOrder(orderId); + return Response.ok(shipments).build(); + } + + /** + * Gets a shipment by tracking number. + * + * @param trackingNumber The tracking number + * @return The shipment + */ + @GET + @Path("/tracking/{trackingNumber}") + @Operation(summary = "Get a shipment by tracking number") + @APIResponse(responseCode = "200", description = "Shipment found", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response getShipmentByTrackingNumber( + @Parameter(description = "Tracking number", required = true) + @PathParam("trackingNumber") String trackingNumber) { + + LOGGER.info("REST request to get shipment with tracking number: " + trackingNumber); + + Optional shipment = shipmentService.getShipmentByTrackingNumber(trackingNumber); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the status of a shipment. + * + * @param shipmentId The shipment ID + * @param status The new status + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/status/{status}") + @Operation(summary = "Update shipment status") + @APIResponse(responseCode = "200", description = "Shipment status updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateShipmentStatus( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New status", required = true) + @PathParam("status") ShipmentStatus status) { + + LOGGER.info("REST request to update shipment " + shipmentId + " status to " + status); + + Optional shipment = shipmentService.updateShipmentStatus(shipmentId, status); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the carrier for a shipment. + * + * @param shipmentId The shipment ID + * @param carrier The new carrier + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/carrier") + @Operation(summary = "Update shipment carrier") + @APIResponse(responseCode = "200", description = "Carrier updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateCarrier( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New carrier", required = true) + @NotNull String carrier) { + + LOGGER.info("REST request to update carrier for shipment " + shipmentId + " to " + carrier); + + Optional shipment = shipmentService.updateCarrier(shipmentId, carrier); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the tracking number for a shipment. + * + * @param shipmentId The shipment ID + * @param trackingNumber The new tracking number + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/tracking") + @Operation(summary = "Update shipment tracking number") + @APIResponse(responseCode = "200", description = "Tracking number updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateTrackingNumber( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New tracking number", required = true) + @NotNull String trackingNumber) { + + LOGGER.info("REST request to update tracking number for shipment " + shipmentId + " to " + trackingNumber); + + Optional shipment = shipmentService.updateTrackingNumber(shipmentId, trackingNumber); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the estimated delivery date for a shipment. + * + * @param shipmentId The shipment ID + * @param dateStr The new estimated delivery date (ISO format) + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/delivery-date") + @Operation(summary = "Update shipment estimated delivery date") + @APIResponse(responseCode = "200", description = "Estimated delivery date updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "400", description = "Invalid date format") + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateEstimatedDelivery( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New estimated delivery date (ISO format: yyyy-MM-dd'T'HH:mm:ss)", required = true) + @NotNull String dateStr) { + + LOGGER.info("REST request to update estimated delivery for shipment " + shipmentId + " to " + dateStr); + + try { + LocalDateTime date = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + Optional shipment = shipmentService.updateEstimatedDelivery(shipmentId, date); + + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Invalid date format. Use ISO format: yyyy-MM-dd'T'HH:mm:ss\"}") + .build(); + } + } + + /** + * Updates the notes for a shipment. + * + * @param shipmentId The shipment ID + * @param notes The new notes + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/notes") + @Operation(summary = "Update shipment notes") + @APIResponse(responseCode = "200", description = "Notes updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateNotes( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New notes", required = true) + String notes) { + + LOGGER.info("REST request to update notes for shipment " + shipmentId); + + Optional shipment = shipmentService.updateNotes(shipmentId, notes); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Deletes a shipment. + * + * @param shipmentId The shipment ID + * @return A response indicating success or failure + */ + @DELETE + @Path("/{shipmentId}") + @Operation(summary = "Delete a shipment") + @APIResponse(responseCode = "204", description = "Shipment deleted") + @APIResponse(responseCode = "404", description = "Shipment not found") + @APIResponse(responseCode = "400", description = "Shipment cannot be deleted due to its status") + public Response deleteShipment( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId) { + + LOGGER.info("REST request to delete shipment: " + shipmentId); + + boolean deleted = shipmentService.deleteShipment(shipmentId); + if (deleted) { + return Response.noContent().build(); + } + + // Check if shipment exists but cannot be deleted due to its status + Optional shipment = shipmentService.getShipment(shipmentId); + if (shipment.isPresent()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Shipment cannot be deleted due to its status\"}") + .build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java new file mode 100644 index 00000000..f29aadee --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java @@ -0,0 +1,305 @@ +package io.microprofile.tutorial.store.shipment.service; + +import io.microprofile.tutorial.store.shipment.client.OrderClient; +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; +import io.microprofile.tutorial.store.shipment.repository.ShipmentRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Timed; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.logging.Logger; + +/** + * Shipment Service for managing shipments. + */ +@ApplicationScoped +public class ShipmentService { + + private static final Logger LOGGER = Logger.getLogger(ShipmentService.class.getName()); + private static final Random RANDOM = new Random(); + private static final String[] CARRIERS = {"FedEx", "UPS", "USPS", "DHL", "Amazon Logistics"}; + + @Inject + private ShipmentRepository shipmentRepository; + + @Inject + private OrderClient orderClient; + + /** + * Creates a new shipment for an order. + * + * @param orderId The order ID + * @return The created shipment, or null if the order is invalid + */ + @Counted(name = "shipmentCreations", description = "Number of shipments created") + @Timed(name = "createShipmentTimer", description = "Time to create a shipment") + public Shipment createShipment(Long orderId) { + LOGGER.info("Creating shipment for order: " + orderId); + + // Verify that the order exists and is ready for shipment + if (!orderClient.verifyOrder(orderId)) { + LOGGER.warning("Order " + orderId + " is not valid for shipment"); + return null; + } + + // Get shipping address from order service + String shippingAddress = orderClient.getShippingAddress(orderId); + if (shippingAddress == null) { + LOGGER.warning("Could not retrieve shipping address for order " + orderId); + return null; + } + + // Create a new shipment + Shipment shipment = Shipment.builder() + .orderId(orderId) + .status(ShipmentStatus.PENDING) + .trackingNumber(generateTrackingNumber()) + .carrier(selectRandomCarrier()) + .shippingAddress(shippingAddress) + .estimatedDelivery(LocalDateTime.now().plusDays(5)) + .createdAt(LocalDateTime.now()) + .build(); + + Shipment savedShipment = shipmentRepository.save(shipment); + + // Update order status to indicate shipment is being processed + orderClient.updateOrderStatus(orderId, "SHIPMENT_CREATED"); + + return savedShipment; + } + + /** + * Updates the status of a shipment. + * + * @param shipmentId The shipment ID + * @param status The new status + * @return The updated shipment, or empty if not found + */ + @Counted(name = "shipmentStatusUpdates", description = "Number of shipment status updates") + public Optional updateShipmentStatus(Long shipmentId, ShipmentStatus status) { + LOGGER.info("Updating shipment " + shipmentId + " status to " + status); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setStatus(status); + shipment.setUpdatedAt(LocalDateTime.now()); + + // If status is SHIPPED, set the shipped date + if (status == ShipmentStatus.SHIPPED) { + shipment.setShippedAt(LocalDateTime.now()); + orderClient.updateOrderStatus(shipment.getOrderId(), "SHIPPED"); + } + // If status is DELIVERED, update order status + else if (status == ShipmentStatus.DELIVERED) { + orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERED"); + } + // If status is FAILED, update order status + else if (status == ShipmentStatus.FAILED) { + orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERY_FAILED"); + } + + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Gets a shipment by ID. + * + * @param shipmentId The shipment ID + * @return The shipment, or empty if not found + */ + public Optional getShipment(Long shipmentId) { + LOGGER.info("Getting shipment: " + shipmentId); + return shipmentRepository.findById(shipmentId); + } + + /** + * Gets all shipments for an order. + * + * @param orderId The order ID + * @return The list of shipments for the order + */ + public List getShipmentsByOrder(Long orderId) { + LOGGER.info("Getting shipments for order: " + orderId); + return shipmentRepository.findByOrderId(orderId); + } + + /** + * Gets a shipment by tracking number. + * + * @param trackingNumber The tracking number + * @return The shipment, or empty if not found + */ + public Optional getShipmentByTrackingNumber(String trackingNumber) { + LOGGER.info("Getting shipment with tracking number: " + trackingNumber); + List shipments = shipmentRepository.findByTrackingNumber(trackingNumber); + return shipments.isEmpty() ? Optional.empty() : Optional.of(shipments.get(0)); + } + + /** + * Gets all shipments. + * + * @return All shipments + */ + public List getAllShipments() { + LOGGER.info("Getting all shipments"); + return shipmentRepository.findAll(); + } + + /** + * Gets shipments by status. + * + * @param status The status + * @return The list of shipments with the given status + */ + public List getShipmentsByStatus(ShipmentStatus status) { + LOGGER.info("Getting shipments with status: " + status); + return shipmentRepository.findByStatus(status); + } + + /** + * Gets shipments due for delivery by the given date. + * + * @param date The date + * @return The list of shipments due by the given date + */ + public List getShipmentsDueBy(LocalDateTime date) { + LOGGER.info("Getting shipments due by: " + date); + return shipmentRepository.findByEstimatedDeliveryBefore(date); + } + + /** + * Updates the carrier for a shipment. + * + * @param shipmentId The shipment ID + * @param carrier The new carrier + * @return The updated shipment, or empty if not found + */ + public Optional updateCarrier(Long shipmentId, String carrier) { + LOGGER.info("Updating carrier for shipment " + shipmentId + " to " + carrier); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setCarrier(carrier); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the tracking number for a shipment. + * + * @param shipmentId The shipment ID + * @param trackingNumber The new tracking number + * @return The updated shipment, or empty if not found + */ + public Optional updateTrackingNumber(Long shipmentId, String trackingNumber) { + LOGGER.info("Updating tracking number for shipment " + shipmentId + " to " + trackingNumber); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setTrackingNumber(trackingNumber); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the estimated delivery date for a shipment. + * + * @param shipmentId The shipment ID + * @param estimatedDelivery The new estimated delivery date + * @return The updated shipment, or empty if not found + */ + public Optional updateEstimatedDelivery(Long shipmentId, LocalDateTime estimatedDelivery) { + LOGGER.info("Updating estimated delivery for shipment " + shipmentId + " to " + estimatedDelivery); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setEstimatedDelivery(estimatedDelivery); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the notes for a shipment. + * + * @param shipmentId The shipment ID + * @param notes The new notes + * @return The updated shipment, or empty if not found + */ + public Optional updateNotes(Long shipmentId, String notes) { + LOGGER.info("Updating notes for shipment " + shipmentId); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setNotes(notes); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Deletes a shipment. + * + * @param shipmentId The shipment ID + * @return true if the shipment was deleted, false if not found + */ + public boolean deleteShipment(Long shipmentId) { + LOGGER.info("Deleting shipment: " + shipmentId); + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + // Only allow deletion if the shipment is in PENDING or PROCESSING status + ShipmentStatus status = shipmentOpt.get().getStatus(); + if (status == ShipmentStatus.PENDING || status == ShipmentStatus.PROCESSING) { + return shipmentRepository.deleteById(shipmentId); + } + LOGGER.warning("Cannot delete shipment with status: " + status); + return false; + } + return false; + } + + /** + * Generates a random tracking number. + * + * @return A random tracking number + */ + private String generateTrackingNumber() { + return String.format("%s-%04d-%04d-%04d", + CARRIERS[RANDOM.nextInt(CARRIERS.length)].substring(0, 2).toUpperCase(), + RANDOM.nextInt(10000), + RANDOM.nextInt(10000), + RANDOM.nextInt(10000)); + } + + /** + * Selects a random carrier. + * + * @return A random carrier + */ + private String selectRandomCarrier() { + return CARRIERS[RANDOM.nextInt(CARRIERS.length)]; + } +} diff --git a/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..5057c129 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,32 @@ +# Shipment Service Configuration + +# Order Service URL +order.service.url=http://localhost:8050/order + +# Configure health check properties +mp.health.check.timeout=5s + +# Configure default MP Metrics properties +mp.metrics.tags=app=shipment-service + +# Configure fault tolerance policies +# Retry configuration +mp.fault.tolerance.Retry.delay=1000 +mp.fault.tolerance.Retry.maxRetries=3 +mp.fault.tolerance.Retry.jitter=200 + +# Timeout configuration +mp.fault.tolerance.Timeout.value=5000 + +# Circuit Breaker configuration +mp.fault.tolerance.CircuitBreaker.requestVolumeThreshold=5 +mp.fault.tolerance.CircuitBreaker.failureRatio=0.5 +mp.fault.tolerance.CircuitBreaker.delay=10000 +mp.fault.tolerance.CircuitBreaker.successThreshold=2 + +# Open API configuration +mp.openapi.scan.disable=false +mp.openapi.scan.packages=io.microprofile.tutorial.store.shipment + +# In Docker environment, override the Order service URL +%docker.order.service.url=http://order:8050/order diff --git a/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ed5b091b --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + Shipment Service + + + index.html + + + diff --git a/code/chapter11/chapter11/shipment/src/main/webapp/index.html b/code/chapter11/chapter11/shipment/src/main/webapp/index.html new file mode 100644 index 00000000..5641acb7 --- /dev/null +++ b/code/chapter11/chapter11/shipment/src/main/webapp/index.html @@ -0,0 +1,150 @@ + + + + + + Shipment Service - MicroProfile Tutorial + + + +

Shipment Service

+

+ This is the Shipment Service for the MicroProfile Tutorial e-commerce application. + The service manages shipments for orders in the system. +

+ +

REST API

+

+ The service exposes the following endpoints: +

+ +
+

POST /api/shipments/orders/{orderId}

+

Create a new shipment for an order.

+
+ +
+

GET /api/shipments/{shipmentId}

+

Get a shipment by ID.

+
+ +
+

GET /api/shipments

+

Get all shipments.

+
+ +
+

GET /api/shipments/status/{status}

+

Get shipments by status (e.g., PENDING, PROCESSING, SHIPPED, etc.).

+
+ +
+

GET /api/shipments/orders/{orderId}

+

Get all shipments for an order.

+
+ +
+

GET /api/shipments/tracking/{trackingNumber}

+

Get a shipment by tracking number.

+
+ +
+

PUT /api/shipments/{shipmentId}/status/{status}

+

Update the status of a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/carrier

+

Update the carrier for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/tracking

+

Update the tracking number for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/delivery-date

+

Update the estimated delivery date for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/notes

+

Update the notes for a shipment.

+
+ +
+

DELETE /api/shipments/{shipmentId}

+

Delete a shipment (only allowed for shipments in PENDING or PROCESSING status).

+
+ +

OpenAPI Documentation

+

+ The service provides OpenAPI documentation at /shipment/openapi. + You can also access the Swagger UI at /shipment/openapi/ui. +

+ +

Health Checks

+

+ MicroProfile Health endpoints are available at: +

+ + +

Metrics

+

+ MicroProfile Metrics are available at /shipment/metrics. +

+ +
+

Shipment Service - MicroProfile Tutorial E-commerce Application

+
+ + diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java new file mode 100644 index 00000000..e13684cc --- /dev/null +++ b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java @@ -0,0 +1,184 @@ +package io.microprofile.tutorial.store.shoppingcart.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Catalog Service. + */ +@ApplicationScoped +public class CatalogClient { + + private static final Logger LOGGER = Logger.getLogger(CatalogClient.class.getName()); + + @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog") + private String catalogServiceUrl; + + // Cache for product details to reduce service calls + private final Map productCache = new HashMap<>(); + + /** + * Gets product information from the catalog service. + * + * @param productId The product ID + * @return ProductInfo containing product details + */ + // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) + // @Timeout(value = 5, unit = ChronoUnit.SECONDS) + // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + // @Fallback(fallbackMethod = "getProductInfoFallback") + public ProductInfo getProductInfo(Long productId) { + // Check cache first + if (productCache.containsKey(productId)) { + return productCache.get(productId); + } + + LOGGER.info(String.format("Fetching product info for product %d", productId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/products/%d", catalogServiceUrl, productId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple parsing - in a real app, use proper JSON parsing + String name = extractField(jsonResponse, "name"); + String priceStr = extractField(jsonResponse, "price"); + + double price = 0.0; + try { + price = Double.parseDouble(priceStr); + } catch (NumberFormatException e) { + LOGGER.warning("Failed to parse product price: " + priceStr); + } + + ProductInfo productInfo = new ProductInfo(productId, name, price); + + // Cache the result + productCache.put(productId, productInfo); + + return productInfo; + } + + LOGGER.warning(String.format("Failed to get product info. Status code: %d", response.getStatus())); + return new ProductInfo(productId, "Unknown Product", 0.0); + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Catalog Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for getProductInfo. + * Returns a placeholder product info when the catalog service is unavailable. + * + * @param productId The product ID + * @return A placeholder ProductInfo object + */ + public ProductInfo getProductInfoFallback(Long productId) { + LOGGER.warning(String.format("Using fallback for product info. Product ID: %d", productId)); + + // Check if we have a cached version + if (productCache.containsKey(productId)) { + return productCache.get(productId); + } + + // Return a placeholder + return new ProductInfo( + productId, + "Product " + productId + " (Service Unavailable)", + 0.0 + ); + } + + /** + * Helper method to extract field values from JSON string. + * This is a simplified approach - in a real app, use a proper JSON parser. + * + * @param jsonString The JSON string + * @param fieldName The name of the field to extract + * @return The extracted field value + */ + private String extractField(String jsonString, String fieldName) { + String searchPattern = "\"" + fieldName + "\":"; + if (jsonString.contains(searchPattern)) { + int startIndex = jsonString.indexOf(searchPattern) + searchPattern.length(); + int endIndex; + + // Skip whitespace + while (startIndex < jsonString.length() && + (jsonString.charAt(startIndex) == ' ' || jsonString.charAt(startIndex) == '\t')) { + startIndex++; + } + + if (startIndex < jsonString.length() && jsonString.charAt(startIndex) == '"') { + // String value + startIndex++; // Skip opening quote + endIndex = jsonString.indexOf("\"", startIndex); + } else { + // Number or boolean value + endIndex = jsonString.indexOf(",", startIndex); + if (endIndex == -1) { + endIndex = jsonString.indexOf("}", startIndex); + } + } + + if (endIndex > startIndex) { + return jsonString.substring(startIndex, endIndex); + } + } + return ""; + } + + /** + * Inner class to hold product information. + */ + public static class ProductInfo { + private final Long productId; + private final String name; + private final double price; + + public ProductInfo(Long productId, String name, double price) { + this.productId = productId; + this.name = name; + this.price = price; + } + + public Long getProductId() { + return productId; + } + + public String getName() { + return name; + } + + public double getPrice() { + return price; + } + } +} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java new file mode 100644 index 00000000..b9ac4c04 --- /dev/null +++ b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java @@ -0,0 +1,96 @@ +package io.microprofile.tutorial.store.shoppingcart.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Inventory Service. + */ +@ApplicationScoped +public class InventoryClient { + + private static final Logger LOGGER = Logger.getLogger(InventoryClient.class.getName()); + + @ConfigProperty(name = "inventory.service.url", defaultValue = "http://localhost:7050/inventory") + private String inventoryServiceUrl; + + /** + * Checks if a product is available in sufficient quantity. + * + * @param productId The product ID + * @param quantity The requested quantity + * @return true if the product is available in the requested quantity, false otherwise + */ + // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) + // @Timeout(value = 5, unit = ChronoUnit.SECONDS) + // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + // @Fallback(fallbackMethod = "checkProductAvailabilityFallback") + public boolean checkProductAvailability(Long productId, int quantity) { + LOGGER.info(String.format("Checking availability for product %d, quantity %d", productId, quantity)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/inventories/product/%d", inventoryServiceUrl, productId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple parsing - in a real app, use proper JSON parsing + if (jsonResponse.contains("\"quantity\":")) { + String quantityStr = jsonResponse.split("\"quantity\":")[1].split(",")[0].trim(); + int availableQuantity = Integer.parseInt(quantityStr); + return availableQuantity >= quantity; + } + } + + LOGGER.warning(String.format("Failed to check product availability. Status code: %d", response.getStatus())); + return false; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Inventory Service", e); + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error parsing inventory response", e); + return false; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for checkProductAvailability. + * Always returns true to allow the cart operation to continue, + * but logs a warning. + * + * @param productId The product ID + * @param quantity The requested quantity + * @return true, allowing the operation to proceed + */ + public boolean checkProductAvailabilityFallback(Long productId, int quantity) { + LOGGER.warning(String.format( + "Using fallback for product availability check. Product ID: %d, Quantity: %d", + productId, quantity)); + // In a production system, you might want to cache product availability + // or implement a more sophisticated fallback mechanism + return true; // Allow the operation to proceed + } +} From 51d52b5961456fd7217cbe60a22da486ea06af67 Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 18:15:29 +0000 Subject: [PATCH 6/7] Delete chapter 11 --- .../client/ProductServiceClient.java | 23 - .../store/inventory/entity/Inventory.java | 51 -- .../inventory/exception/ErrorResponse.java | 103 ---- .../exception/InventoryConflictException.java | 41 -- .../exception/InventoryExceptionMapper.java | 46 -- .../exception/InventoryNotFoundException.java | 40 -- .../inventory/resource/InventoryResource.java | 266 ---------- .../inventory/service/InventoryService.java | 493 ------------------ code/chapter11/chapter11/shipment/Dockerfile | 27 - code/chapter11/chapter11/shipment/README.md | 87 ---- code/chapter11/chapter11/shipment/pom.xml | 114 ---- .../chapter11/shipment/run-docker.sh | 11 - code/chapter11/chapter11/shipment/run.sh | 12 - .../store/shipment/ShipmentApplication.java | 35 -- .../store/shipment/client/OrderClient.java | 193 ------- .../store/shipment/entity/Shipment.java | 45 -- .../store/shipment/entity/ShipmentStatus.java | 16 - .../store/shipment/filter/CorsFilter.java | 43 -- .../shipment/health/ShipmentHealthCheck.java | 67 --- .../repository/ShipmentRepository.java | 148 ------ .../shipment/resource/ShipmentResource.java | 397 -------------- .../shipment/service/ShipmentService.java | 305 ----------- .../META-INF/microprofile-config.properties | 32 -- .../shipment/src/main/webapp/WEB-INF/web.xml | 13 - .../shipment/src/main/webapp/index.html | 150 ------ .../chapter11/shoppingcart/Dockerfile | 20 - .../chapter11/shoppingcart/README.md | 87 ---- code/chapter11/chapter11/shoppingcart/pom.xml | 114 ---- .../chapter11/shoppingcart/run-docker.sh | 23 - code/chapter11/chapter11/shoppingcart/run.sh | 19 - .../shoppingcart/ShoppingCartApplication.java | 12 - .../shoppingcart/client/CatalogClient.java | 184 ------- .../shoppingcart/client/InventoryClient.java | 96 ---- .../store/shoppingcart/entity/CartItem.java | 32 -- .../shoppingcart/entity/ShoppingCart.java | 57 -- .../health/ShoppingCartHealthCheck.java | 68 --- .../repository/ShoppingCartRepository.java | 199 ------- .../resource/ShoppingCartResource.java | 240 --------- .../service/ShoppingCartService.java | 223 -------- .../META-INF/microprofile-config.properties | 16 - .../src/main/webapp/WEB-INF/web.xml | 12 - .../shoppingcart/src/main/webapp/index.html | 128 ----- .../shoppingcart/src/main/webapp/index.jsp | 12 - 43 files changed, 4300 deletions(-) delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java delete mode 100644 code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java delete mode 100644 code/chapter11/chapter11/shipment/Dockerfile delete mode 100644 code/chapter11/chapter11/shipment/README.md delete mode 100644 code/chapter11/chapter11/shipment/pom.xml delete mode 100644 code/chapter11/chapter11/shipment/run-docker.sh delete mode 100644 code/chapter11/chapter11/shipment/run.sh delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java delete mode 100644 code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/chapter11/shipment/src/main/webapp/index.html delete mode 100644 code/chapter11/chapter11/shoppingcart/Dockerfile delete mode 100644 code/chapter11/chapter11/shoppingcart/README.md delete mode 100644 code/chapter11/chapter11/shoppingcart/pom.xml delete mode 100644 code/chapter11/chapter11/shoppingcart/run-docker.sh delete mode 100644 code/chapter11/chapter11/shoppingcart/run.sh delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html delete mode 100644 code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java deleted file mode 100644 index 6aee3f2e..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.microprofile.tutorial.store.inventory.client; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.QueryParam; -import java.util.List; - -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; - -import io.microprofile.tutorial.store.inventory.dto.Product; - -@RegisterRestClient(configKey = "product-service") -@Path("/products") -public interface ProductServiceClient extends AutoCloseable { - - @GET - @Path("/{id}") - Product getProductById(@PathParam("id") Long id); - - @GET - List getProductsByCategory(@QueryParam("category") String category); -} \ No newline at end of file diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java deleted file mode 100644 index 6b7b9693..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.microprofile.tutorial.store.inventory.entity; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Inventory class for the microprofile tutorial store application. - * This class represents inventory information for products in the system. - * We're using an in-memory data structure rather than a database. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Inventory { - - /** - * Unique identifier for the inventory record. - * This can be null for new records before they are persisted. - */ - private Long inventoryId; - - /** - * Reference to the product this inventory record belongs to. - * Must not be null to maintain data integrity. - */ - @NotNull(message = "Product ID cannot be null") - private Long productId; - - /** - * Current quantity of the product available in inventory. - * Must not be null and must be non-negative. - */ - @NotNull(message = "Quantity cannot be null") - @Min(value = 0, message = "Quantity must be greater than or equal to 0") - private Integer quantity; - - /** - * Quantity of the product that is reserved (e.g., in pending orders). - * Must not be null and must be non-negative. - */ - @NotNull(message = "Reserved quantity cannot be null") - @Min(value = 0, message = "Reserved quantity must be greater than or equal to 0") - @Builder.Default - private Integer reservedQuantity = 0; -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java deleted file mode 100644 index c99ad4d4..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.microprofile.tutorial.store.inventory.exception; - -import java.util.HashMap; -import java.util.Map; - -/** - * Represents an error response to be returned to the client. - * Used for formatting error messages in a consistent way. - */ -public class ErrorResponse { - private String errorCode; - private String message; - private Map details; - - /** - * Constructs a new ErrorResponse with the specified error code and message. - * - * @param errorCode a code identifying the error type - * @param message a human-readable error message - */ - public ErrorResponse(String errorCode, String message) { - this.errorCode = errorCode; - this.message = message; - this.details = new HashMap<>(); - } - - /** - * Constructs a new ErrorResponse with the specified error code, message, and details. - * - * @param errorCode a code identifying the error type - * @param message a human-readable error message - * @param details additional information about the error - */ - public ErrorResponse(String errorCode, String message, Map details) { - this.errorCode = errorCode; - this.message = message; - this.details = details; - } - - /** - * Gets the error code. - * - * @return the error code - */ - public String getErrorCode() { - return errorCode; - } - - /** - * Sets the error code. - * - * @param errorCode the error code to set - */ - public void setErrorCode(String errorCode) { - this.errorCode = errorCode; - } - - /** - * Gets the error message. - * - * @return the error message - */ - public String getMessage() { - return message; - } - - /** - * Sets the error message. - * - * @param message the error message to set - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * Gets the error details. - * - * @return the error details - */ - public Map getDetails() { - return details; - } - - /** - * Sets the error details. - * - * @param details the error details to set - */ - public void setDetails(Map details) { - this.details = details; - } - - /** - * Adds a detail to the error response. - * - * @param key the detail key - * @param value the detail value - */ - public void addDetail(String key, Object value) { - this.details.put(key, value); - } -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java deleted file mode 100644 index 22010349..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.microprofile.tutorial.store.inventory.exception; - -import jakarta.ws.rs.core.Response; - -/** - * Exception thrown when there is a conflict with an inventory operation, - * such as when trying to create an inventory for a product that already has one. - */ -public class InventoryConflictException extends RuntimeException { - private Response.Status status; - - /** - * Constructs a new InventoryConflictException with the specified message. - * - * @param message the detail message - */ - public InventoryConflictException(String message) { - super(message); - this.status = Response.Status.CONFLICT; - } - - /** - * Constructs a new InventoryConflictException with the specified message and status. - * - * @param message the detail message - * @param status the HTTP status code to return - */ - public InventoryConflictException(String message, Response.Status status) { - super(message); - this.status = status; - } - - /** - * Gets the HTTP status associated with this exception. - * - * @return the HTTP status - */ - public Response.Status getStatus() { - return status; - } -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java deleted file mode 100644 index 224062eb..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.microprofile.tutorial.store.inventory.exception; - -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.ExceptionMapper; -import jakarta.ws.rs.ext.Provider; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Exception mapper for handling all runtime exceptions in the inventory service. - * Maps exceptions to appropriate HTTP responses with formatted error messages. - */ -@Provider -public class InventoryExceptionMapper implements ExceptionMapper { - - private static final Logger LOGGER = Logger.getLogger(InventoryExceptionMapper.class.getName()); - - @Override - public Response toResponse(RuntimeException exception) { - if (exception instanceof InventoryNotFoundException) { - InventoryNotFoundException notFoundException = (InventoryNotFoundException) exception; - LOGGER.log(Level.INFO, "Resource not found: {0}", exception.getMessage()); - - return Response.status(notFoundException.getStatus()) - .entity(new ErrorResponse("not_found", exception.getMessage())) - .type(MediaType.APPLICATION_JSON) - .build(); - } else if (exception instanceof InventoryConflictException) { - InventoryConflictException conflictException = (InventoryConflictException) exception; - LOGGER.log(Level.INFO, "Resource conflict: {0}", exception.getMessage()); - - return Response.status(conflictException.getStatus()) - .entity(new ErrorResponse("conflict", exception.getMessage())) - .type(MediaType.APPLICATION_JSON) - .build(); - } - - // Handle unexpected exceptions - LOGGER.log(Level.SEVERE, "Unexpected error", exception); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(new ErrorResponse("server_error", "An unexpected error occurred")) - .type(MediaType.APPLICATION_JSON) - .build(); - } -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java deleted file mode 100644 index 991d6330..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.microprofile.tutorial.store.inventory.exception; - -import jakarta.ws.rs.core.Response; - -/** - * Exception thrown when an inventory item is not found. - */ -public class InventoryNotFoundException extends RuntimeException { - private Response.Status status; - - /** - * Constructs a new InventoryNotFoundException with the specified message. - * - * @param message the detail message - */ - public InventoryNotFoundException(String message) { - super(message); - this.status = Response.Status.NOT_FOUND; - } - - /** - * Constructs a new InventoryNotFoundException with the specified message and status. - * - * @param message the detail message - * @param status the HTTP status code to return - */ - public InventoryNotFoundException(String message, Response.Status status) { - super(message); - this.status = status; - } - - /** - * Gets the HTTP status associated with this exception. - * - * @return the HTTP status - */ - public Response.Status getStatus() { - return status; - } -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java deleted file mode 100644 index 32189b02..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java +++ /dev/null @@ -1,266 +0,0 @@ -package io.microprofile.tutorial.store.inventory.resource; - -import io.microprofile.tutorial.store.inventory.entity.Inventory; -import io.microprofile.tutorial.store.inventory.service.InventoryService; -import io.microprofile.tutorial.store.inventory.dto.Product; - -import java.net.URI; -import java.util.List; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * REST resource for inventory operations. - */ -@Path("/inventories") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Inventory", description = "Operations related to product inventory management") -public class InventoryResource { - - @Inject - private InventoryService inventoryService; - - @Context - private UriInfo uriInfo; - - @GET - @Operation(summary = "Get all inventory items", description = "Returns a paginated list of inventory items with optional filtering") - @APIResponse( - responseCode = "200", - description = "List of inventory items", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Inventory.class) - ) - ) - public Response getAllInventories( - @Parameter(description = "Page number (zero-based)", schema = @Schema(defaultValue = "0")) - @QueryParam("page") @DefaultValue("0") int page, - - @Parameter(description = "Page size", schema = @Schema(defaultValue = "20")) - @QueryParam("size") @DefaultValue("20") int size, - - @Parameter(description = "Filter by minimum quantity") - @QueryParam("minQuantity") Integer minQuantity, - - @Parameter(description = "Filter by maximum quantity") - @QueryParam("maxQuantity") Integer maxQuantity) { - - List inventories = inventoryService.getAllInventories(page, size, minQuantity, maxQuantity); - long totalCount = inventoryService.countInventories(minQuantity, maxQuantity); - - return Response.ok(inventories) - .header("X-Total-Count", totalCount) - .header("X-Page-Number", page) - .header("X-Page-Size", size) - .build(); - } - - @GET - @Path("/{id}") - @Operation(summary = "Get inventory item by ID", description = "Returns a specific inventory item by ID") - @APIResponse( - responseCode = "200", - description = "Inventory item", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Inventory not found" - ) - public Inventory getInventoryById( - @Parameter(description = "ID of the inventory item", required = true) - @PathParam("id") Long id) { - return inventoryService.getInventoryById(id); - } - - @GET - @Path("/product/{productId}") - @Operation(summary = "Get inventory item by product ID", description = "Returns inventory information for a specific product") - @APIResponse( - responseCode = "200", - description = "Inventory item", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Inventory not found for product" - ) - public Inventory getInventoryByProductId( - @Parameter(description = "Product ID", required = true) - @PathParam("productId") Long productId) { - return inventoryService.getInventoryByProductId(productId); - } - - @POST - @Operation(summary = "Create new inventory item", description = "Creates a new inventory item") - @APIResponse( - responseCode = "201", - description = "Inventory created", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "409", - description = "Inventory for product already exists" - ) - public Response createInventory( - @Parameter(description = "Inventory details", required = true) - @NotNull @Valid Inventory inventory) { - Inventory createdInventory = inventoryService.createInventory(inventory); - URI location = uriInfo.getAbsolutePathBuilder().path(createdInventory.getInventoryId().toString()).build(); - return Response.created(location).entity(createdInventory).build(); - } - - @PUT - @Path("/{id}") - @Operation(summary = "Update inventory item", description = "Updates an existing inventory item") - @APIResponse( - responseCode = "200", - description = "Inventory updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Inventory not found" - ) - @APIResponse( - responseCode = "409", - description = "Another inventory record already exists for this product" - ) - public Inventory updateInventory( - @Parameter(description = "ID of the inventory item", required = true) - @PathParam("id") Long id, - @Parameter(description = "Updated inventory details", required = true) - @NotNull @Valid Inventory inventory) { - return inventoryService.updateInventory(id, inventory); - } - - @DELETE - @Path("/{id}") - @Operation(summary = "Delete inventory item", description = "Deletes an inventory item") - @APIResponse( - responseCode = "204", - description = "Inventory deleted" - ) - @APIResponse( - responseCode = "404", - description = "Inventory not found" - ) - public Response deleteInventory( - @Parameter(description = "ID of the inventory item", required = true) - @PathParam("id") Long id) { - inventoryService.deleteInventory(id); - return Response.noContent().build(); - } - - @PATCH - @Path("/product/{productId}/quantity/{quantity}") - @Operation(summary = "Update product quantity", description = "Updates the quantity for a specific product") - @APIResponse( - responseCode = "200", - description = "Quantity updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Inventory not found for product" - ) - public Inventory updateQuantity( - @Parameter(description = "Product ID", required = true) - @PathParam("productId") Long productId, - @Parameter(description = "New quantity", required = true) - @PathParam("quantity") int quantity) { - return inventoryService.updateQuantity(productId, quantity); - } - - @PATCH - @Path("/product/{productId}/reserve/{quantity}") - @Operation(summary = "Reserve inventory for a product", - description = "Reserves the specified quantity of inventory for a product if it's available in the catalog") - @APIResponse( - responseCode = "200", - description = "Inventory reserved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Inventory.class) - ) - ) - @APIResponse( - responseCode = "400", - description = "Invalid quantity or insufficient inventory available" - ) - @APIResponse( - responseCode = "404", - description = "Product not found in catalog or inventory not found" - ) - public Inventory reserveInventory( - @Parameter(description = "Product ID", required = true) - @PathParam("productId") Long productId, - @Parameter(description = "Quantity to reserve", required = true) - @PathParam("quantity") int quantity) { - return inventoryService.reserveInventory(productId, quantity); - } - - @GET - @Path("/product-info/{productId}") - @Operation(summary = "Get product information using custom RestClientBuilder", - description = "Demonstrates advanced RestClientBuilder usage with custom timeout configuration") - @APIResponse( - responseCode = "200", - description = "Product information retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Product.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Product not found" - ) - public Response getProductInfo( - @Parameter(description = "Product ID", required = true) - @PathParam("productId") Long productId) { - - Product product = inventoryService.getProductWithCustomClient(productId); - if (product != null) { - return Response.ok(product).build(); - } else { - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"message\": \"Product not found\"}") - .build(); - } - } -} diff --git a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java b/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java deleted file mode 100644 index 3ee718c0..00000000 --- a/code/chapter11/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java +++ /dev/null @@ -1,493 +0,0 @@ -package io.microprofile.tutorial.store.inventory.service; - -import io.microprofile.tutorial.store.inventory.entity.Inventory; -import io.microprofile.tutorial.store.inventory.exception.InventoryConflictException; -import io.microprofile.tutorial.store.inventory.exception.InventoryNotFoundException; -import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; -import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; -import io.microprofile.tutorial.store.inventory.dto.Product; -import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.WebApplicationException; -import jakarta.transaction.Transactional; -import org.eclipse.microprofile.rest.client.RestClientBuilder; -import org.eclipse.microprofile.rest.client.inject.RestClient; - -/** - * Service class for Inventory management operations. - */ -@ApplicationScoped -public class InventoryService { - - private static final Logger LOGGER = Logger.getLogger(InventoryService.class.getName()); - - @Inject - private InventoryRepository inventoryRepository; - - @Inject - @RestClient - private ProductServiceClient productServiceClient; - - /** - * Checks if a product is available in the catalog service. - * This method demonstrates the use of RestClientBuilder for programmatic REST client creation. - * This is a lightweight check that returns only a boolean result. - * - * @param productId The product ID to check - * @return true if the product exists, false otherwise - */ - public boolean isProductAvailable(Long productId) { - LOGGER.fine("Checking product availability for ID: " + productId); - - try { - // Demonstrate RestClientBuilder usage with MP Rest Client 4.0 - // Using new baseUri(String) method - no need for URI.create() - ProductServiceClient dynamicClient = RestClientBuilder.newBuilder() - .baseUri("http://localhost:5050/catalog/api") - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build(ProductServiceClient.class); - - LOGGER.fine("Built dynamic REST client for catalog service using baseUri(String)"); - - Product product = dynamicClient.getProductById(productId); - boolean available = product != null; - LOGGER.fine("Product " + productId + " availability check via RestClientBuilder: " + available); - return available; - - } catch (WebApplicationException e) { - if (e.getResponse().getStatus() == 404) { - LOGGER.fine("Product " + productId + " not found in catalog (via RestClientBuilder)"); - return false; - } - LOGGER.warning("Error checking product availability for ID " + productId + " via RestClientBuilder: " + e.getMessage()); - return false; - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Unexpected error checking product availability for ID " + productId + " via RestClientBuilder", e); - return false; - } - } - - /** - * Validates that a product exists in the catalog service. - * - * @param productId The product ID to validate - * @return The product details if found - * @throws InventoryNotFoundException if the product is not found in the catalog - */ - private Product validateProductExists(Long productId) { - LOGGER.fine("Validating product existence for ID: " + productId); - - try { - Product product = productServiceClient.getProductById(productId); - if (product == null) { - throw new InventoryNotFoundException("Product not found in catalog with ID: " + productId); - } - LOGGER.fine("Product validated successfully: " + product.getName()); - return product; - } catch (InventoryNotFoundException e) { - // Re-throw InventoryNotFoundException without wrapping - throw e; - } catch (WebApplicationException e) { - LOGGER.warning("Product validation failed for ID " + productId + ": " + e.getMessage()); - if (e.getResponse().getStatus() == 404) { - throw new InventoryNotFoundException("Product not found in catalog with ID: " + productId); - } - throw new RuntimeException("Failed to validate product with catalog service: " + e.getMessage(), e); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Unexpected error validating product " + productId, e); - throw new RuntimeException("Failed to validate product with catalog service: " + e.getMessage(), e); - } - } - - /** - * Creates a new inventory item. - * - * @param inventory The inventory to create - * @return The created inventory - * @throws InventoryConflictException if inventory with the product ID already exists - */ - @Transactional - public Inventory createInventory(Inventory inventory) { - LOGGER.info("Creating inventory for product ID: " + inventory.getProductId()); - - // Validate that the product exists in the catalog service - Product product = validateProductExists(inventory.getProductId()); - LOGGER.info("Product validated: " + product.getName() + " (Price: $" + product.getPrice() + ")"); - - // Check if product ID already exists - Optional existingInventory = inventoryRepository.findByProductId(inventory.getProductId()); - if (existingInventory.isPresent()) { - LOGGER.warning("Conflict: Inventory already exists for product ID: " + inventory.getProductId()); - throw new InventoryConflictException("Inventory for product already exists", Response.Status.CONFLICT); - } - - Inventory result = inventoryRepository.save(inventory); - LOGGER.info("Created inventory ID: " + result.getInventoryId() + " for product ID: " + result.getProductId()); - return result; - } - - /** - * Creates new inventory items in bulk. - * - * @param inventories The list of inventories to create - * @return The list of created inventories - * @throws InventoryConflictException if any inventory with the same product ID already exists - */ - @Transactional - public List createBulkInventories(List inventories) { - LOGGER.info("Creating bulk inventories: " + inventories.size() + " items"); - - // Validate products exist in catalog and check for conflicts - for (Inventory inventory : inventories) { - // Validate product exists in catalog - Product product = validateProductExists(inventory.getProductId()); - LOGGER.fine("Product validated for bulk create: " + product.getName() + " (ID: " + inventory.getProductId() + ")"); - - // Check for existing inventory records - Optional existingInventory = inventoryRepository.findByProductId(inventory.getProductId()); - if (existingInventory.isPresent()) { - LOGGER.warning("Conflict detected during bulk create for product ID: " + inventory.getProductId()); - throw new InventoryConflictException("Inventory for product already exists: " + inventory.getProductId()); - } - } - - // Save all inventories - List created = new ArrayList<>(); - for (Inventory inventory : inventories) { - created.add(inventoryRepository.save(inventory)); - } - - LOGGER.info("Successfully created " + created.size() + " inventory items"); - return created; - } - - /** - * Gets an inventory item by ID. - * - * @param id The inventory ID - * @return The inventory - * @throws InventoryNotFoundException if the inventory is not found - */ - public Inventory getInventoryById(Long id) { - LOGGER.fine("Getting inventory by ID: " + id); - return inventoryRepository.findById(id) - .orElseThrow(() -> { - LOGGER.warning("Inventory not found with ID: " + id); - return new InventoryNotFoundException("Inventory not found with ID: " + id); - }); - } - - /** - * Gets inventory by product ID. - * - * @param productId The product ID - * @return The inventory - * @throws InventoryNotFoundException if the inventory is not found - */ - public Inventory getInventoryByProductId(Long productId) { - LOGGER.fine("Getting inventory by product ID: " + productId); - return inventoryRepository.findByProductId(productId) - .orElseThrow(() -> { - LOGGER.warning("Inventory not found for product ID: " + productId); - return new InventoryNotFoundException("Inventory not found for product", Response.Status.NOT_FOUND); - }); - } - - /** - * Gets all inventory items. - * - * @return A list of all inventory items - */ - public List getAllInventories() { - LOGGER.fine("Getting all inventory items"); - return inventoryRepository.findAll(); - } - - /** - * Gets inventory items with pagination and filtering. - * - * @param page Page number (zero-based) - * @param size Page size - * @param minQuantity Minimum quantity filter (optional) - * @param maxQuantity Maximum quantity filter (optional) - * @return A filtered and paginated list of inventory items - */ - public List getAllInventories(int page, int size, Integer minQuantity, Integer maxQuantity) { - LOGGER.fine("Getting inventory items with pagination: page=" + page + ", size=" + size + - ", minQuantity=" + minQuantity + ", maxQuantity=" + maxQuantity); - - // First, get all inventories - List allInventories = inventoryRepository.findAll(); - - // Apply filters if provided - List filteredInventories = allInventories.stream() - .filter(inv -> minQuantity == null || inv.getQuantity() >= minQuantity) - .filter(inv -> maxQuantity == null || inv.getQuantity() <= maxQuantity) - .collect(Collectors.toList()); - - // Apply pagination - int startIndex = page * size; - int endIndex = Math.min(startIndex + size, filteredInventories.size()); - - // Check if the start index is valid - if (startIndex >= filteredInventories.size()) { - return new ArrayList<>(); - } - - return filteredInventories.subList(startIndex, endIndex); - } - - /** - * Counts inventory items with filtering. - * - * @param minQuantity Minimum quantity filter (optional) - * @param maxQuantity Maximum quantity filter (optional) - * @return The count of inventory items that match the filters - */ - public long countInventories(Integer minQuantity, Integer maxQuantity) { - LOGGER.fine("Counting inventory items with filters: minQuantity=" + minQuantity + - ", maxQuantity=" + maxQuantity); - - List allInventories = inventoryRepository.findAll(); - - // Apply filters and count - return allInventories.stream() - .filter(inv -> minQuantity == null || inv.getQuantity() >= minQuantity) - .filter(inv -> maxQuantity == null || inv.getQuantity() <= maxQuantity) - .count(); - } - - /** - * Updates an inventory item. - * - * @param id The inventory ID - * @param inventory The updated inventory information - * @return The updated inventory - * @throws InventoryNotFoundException if the inventory is not found - * @throws InventoryConflictException if another inventory with the same product ID exists - */ - @Transactional - public Inventory updateInventory(Long id, Inventory inventory) { - LOGGER.info("Updating inventory ID: " + id + " for product ID: " + inventory.getProductId()); - - // Validate that the product exists in the catalog service - Product product = validateProductExists(inventory.getProductId()); - LOGGER.info("Product validated for update: " + product.getName() + " (ID: " + product.getId() + ")"); - - // Check if product ID exists in a different inventory record - Optional existingInventoryWithProductId = inventoryRepository.findByProductId(inventory.getProductId()); - if (existingInventoryWithProductId.isPresent() && - !existingInventoryWithProductId.get().getInventoryId().equals(id)) { - LOGGER.warning("Conflict: Another inventory record exists for product ID: " + inventory.getProductId()); - throw new InventoryConflictException("Another inventory record already exists for this product", - Response.Status.CONFLICT); - } - - return inventoryRepository.update(id, inventory) - .orElseThrow(() -> { - LOGGER.warning("Inventory not found with ID: " + id); - return new InventoryNotFoundException("Inventory not found", Response.Status.NOT_FOUND); - }); - } - - /** - * Deletes an inventory item. - * - * @param id The inventory ID - * @throws InventoryNotFoundException if the inventory is not found - */ - @Transactional - public void deleteInventory(Long id) { - LOGGER.info("Deleting inventory with ID: " + id); - boolean deleted = inventoryRepository.deleteById(id); - if (!deleted) { - LOGGER.warning("Inventory not found with ID: " + id); - throw new InventoryNotFoundException("Inventory not found", Response.Status.NOT_FOUND); - } - LOGGER.info("Successfully deleted inventory with ID: " + id); - } - - /** - * Updates the quantity for a product. - * - * @param productId The product ID - * @param quantity The new quantity - * @return The updated inventory - * @throws InventoryNotFoundException if the inventory is not found - */ - @Transactional - public Inventory updateQuantity(Long productId, int quantity) { - if (quantity < 0) { - LOGGER.warning("Invalid quantity: " + quantity + " for product ID: " + productId); - throw new IllegalArgumentException("Quantity cannot be negative"); - } - - LOGGER.info("Updating quantity to " + quantity + " for product ID: " + productId); - Inventory inventory = getInventoryByProductId(productId); - int oldQuantity = inventory.getQuantity(); - inventory.setQuantity(quantity); - - Inventory updated = inventoryRepository.save(inventory); - LOGGER.info("Updated quantity from " + oldQuantity + " to " + quantity + - " for product ID: " + productId + " (inventory ID: " + inventory.getInventoryId() + ")"); - - return updated; - } - - /** - * Gets product information for an inventory item. - * - * @param inventory The inventory item - * @return The product details - */ - public Product getProductInfo(Inventory inventory) { - return validateProductExists(inventory.getProductId()); - } - - /** - * Gets inventory with enriched product information. - * - * @param inventoryId The inventory ID - * @return Inventory with product details - */ - public InventoryWithProductInfo getInventoryWithProductInfo(Long inventoryId) { - Inventory inventory = getInventoryById(inventoryId); - Product product = validateProductExists(inventory.getProductId()); - - return new InventoryWithProductInfo(inventory, product); - } - - /** - * Gets all inventories with product information for a specific category. - * - * @param category The product category - * @return List of inventories for products in the specified category - */ - public List getInventoriesByCategory(String category) { - LOGGER.info("Getting inventories for category: " + category); - - try { - // Get products by category from catalog service - List productsInCategory = productServiceClient.getProductsByCategory(category); - - if (productsInCategory == null || productsInCategory.isEmpty()) { - LOGGER.info("No products found in category: " + category); - return new ArrayList<>(); - } - - // Find inventories for these products - List result = new ArrayList<>(); - for (Product product : productsInCategory) { - try { - Inventory inventory = inventoryRepository.findByProductId(product.getId()).orElse(null); - if (inventory != null) { - result.add(new InventoryWithProductInfo(inventory, product)); - } - } catch (Exception e) { - LOGGER.warning("Error getting inventory for product " + product.getId() + ": " + e.getMessage()); - } - } - - LOGGER.info("Found " + result.size() + " inventory items for category: " + category); - return result; - - } catch (WebApplicationException e) { - LOGGER.warning("Failed to get products by category from catalog service: " + e.getMessage()); - throw new RuntimeException("Failed to retrieve products by category: " + e.getMessage(), e); - } - } - - /** - * Reserves inventory for a product if it's available in the catalog. - * This method uses isProductAvailable for a lightweight check before reservation. - * - * @param productId The product ID - * @param quantityToReserve The quantity to reserve - * @return The updated inventory after reservation - * @throws InventoryNotFoundException if the inventory or product is not found - * @throws IllegalArgumentException if there's insufficient inventory - */ - @Transactional - public Inventory reserveInventory(Long productId, int quantityToReserve) { - if (quantityToReserve <= 0) { - throw new IllegalArgumentException("Quantity to reserve must be positive"); - } - - LOGGER.info("Attempting to reserve " + quantityToReserve + " units for product ID: " + productId); - - // Use isProductAvailable for a lightweight availability check - if (!isProductAvailable(productId)) { - LOGGER.warning("Cannot reserve inventory - product " + productId + " is not available in catalog"); - throw new InventoryNotFoundException("Product is not available in catalog: " + productId); - } - - // Get the current inventory - Inventory inventory = getInventoryByProductId(productId); - - // Check if we have enough inventory to reserve - int availableQuantity = inventory.getQuantity() - inventory.getReservedQuantity(); - if (availableQuantity < quantityToReserve) { - LOGGER.warning("Insufficient inventory to reserve " + quantityToReserve + - " units for product " + productId + ". Available: " + availableQuantity); - throw new IllegalArgumentException("Insufficient inventory available. Requested: " + - quantityToReserve + ", Available: " + availableQuantity); - } - - // Update reserved quantity - inventory.setReservedQuantity(inventory.getReservedQuantity() + quantityToReserve); - - Inventory updated = inventoryRepository.save(inventory); - LOGGER.info("Reserved " + quantityToReserve + " units for product " + productId + - ". New reserved quantity: " + updated.getReservedQuantity()); - - return updated; - } - - /** - * Demonstrates advanced RestClientBuilder usage with custom configuration. - * This method builds a REST client with specific timeout and error handling settings. - * Uses MicroProfile Rest Client 4.0 baseUri(String) convenience method. - * - * @param productId The product ID to check - * @return Product details if found, null otherwise - */ - public Product getProductWithCustomClient(Long productId) { - LOGGER.info("Getting product details using custom RestClientBuilder for ID: " + productId); - - try { - // Build REST client with custom configuration using MP Rest Client 4.0 - // Using baseUri(String) - no URI.create() needed - ProductServiceClient customClient = RestClientBuilder.newBuilder() - .baseUri("http://localhost:5050/catalog/api") - .connectTimeout(3, TimeUnit.SECONDS) // Custom connect timeout - .readTimeout(8, TimeUnit.SECONDS) // Custom read timeout - .build(ProductServiceClient.class); - - LOGGER.info("Built custom REST client with 3s connect and 8s read timeout"); - - Product product = customClient.getProductById(productId); - LOGGER.info("Retrieved product via custom client: " + (product != null ? product.getName() : "null")); - return product; - - } catch (WebApplicationException e) { - LOGGER.warning("WebApplicationException from custom client for product " + productId + - ": Status=" + e.getResponse().getStatus() + ", Message=" + e.getMessage()); - return null; - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Unexpected error from custom REST client for product " + productId, e); - return null; - } - } -} diff --git a/code/chapter11/chapter11/shipment/Dockerfile b/code/chapter11/chapter11/shipment/Dockerfile deleted file mode 100644 index 287b43d0..00000000 --- a/code/chapter11/chapter11/shipment/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM icr.io/appcafe/open-liberty:23.0.0.3-full-java17-openj9-ubi - -# Copy config -COPY --chown=1001:0 src/main/liberty/config/ /config/ - -# Create the app directory -COPY --chown=1001:0 target/shipment.war /config/apps/ - -# Optional: Copy utility scripts -COPY --chown=1001:0 *.sh /opt/ol/helpers/ - -# Environment variables -ENV VERBOSE=true - -# This is important - adds the management of vulnerability databases to allow Docker scanning -RUN dnf install -y shadow-utils - -# Set environment variable for MP config profile -ENV MP_CONFIG_PROFILE=docker - -EXPOSE 8060 9060 - -# Run as non-root user for security -USER 1001 - -# Start Liberty -ENTRYPOINT ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/chapter11/shipment/README.md b/code/chapter11/chapter11/shipment/README.md deleted file mode 100644 index 41619949..00000000 --- a/code/chapter11/chapter11/shipment/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Shipment Service - -This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. - -## Overview - -The Shipment Service is responsible for: -- Creating shipments for orders -- Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) -- Assigning tracking numbers -- Estimating delivery dates -- Communicating with the Order Service to update order status - -## Technologies - -The Shipment Service is built using: -- Jakarta EE 10 -- MicroProfile 6.1 -- Open Liberty -- Java 17 - -## Getting Started - -### Prerequisites - -- JDK 17+ -- Maven 3.8+ -- Docker (for containerized deployment) - -### Running Locally - -To build and run the service: - -```bash -./run.sh -``` - -This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment - -### Running with Docker - -To build and run the service in a Docker container: - -```bash -./run-docker.sh -``` - -This will build a Docker image for the service and run it, exposing ports 8060 and 9060. - -## API Endpoints - -| Method | URL | Description | -|--------|-------------------------------------------|--------------------------------------| -| POST | /api/shipments/orders/{orderId} | Create a new shipment | -| GET | /api/shipments/{shipmentId} | Get a shipment by ID | -| GET | /api/shipments | Get all shipments | -| GET | /api/shipments/status/{status} | Get shipments by status | -| GET | /api/shipments/orders/{orderId} | Get shipments for an order | -| GET | /api/shipments/tracking/{trackingNumber} | Get a shipment by tracking number | -| PUT | /api/shipments/{shipmentId}/status/{status} | Update shipment status | -| PUT | /api/shipments/{shipmentId}/carrier | Update shipment carrier | -| PUT | /api/shipments/{shipmentId}/tracking | Update shipment tracking number | -| PUT | /api/shipments/{shipmentId}/delivery-date | Update estimated delivery date | -| PUT | /api/shipments/{shipmentId}/notes | Update shipment notes | -| DELETE | /api/shipments/{shipmentId} | Delete a shipment | - -## MicroProfile Features - -The service utilizes several MicroProfile features: - -- **Config**: For external configuration -- **Health**: For liveness and readiness checks -- **Metrics**: For monitoring service performance -- **Fault Tolerance**: For resilient communication with the Order Service -- **OpenAPI**: For API documentation - -## Documentation - -API documentation is available at: -- OpenAPI: http://localhost:8060/shipment/openapi -- Swagger UI: http://localhost:8060/shipment/openapi/ui - -## Monitoring - -Health and metrics endpoints: -- Health: http://localhost:8060/shipment/health -- Metrics: http://localhost:8060/shipment/metrics diff --git a/code/chapter11/chapter11/shipment/pom.xml b/code/chapter11/chapter11/shipment/pom.xml deleted file mode 100644 index d5e31f65..00000000 --- a/code/chapter11/chapter11/shipment/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - io.microprofile - shipment - 1.0-SNAPSHOT - war - - shipment-service - https://microprofile.io - - - UTF-8 - 21 - 10.0.0 - 7.1 - 25.0.0.1 - 1.18.36 - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - shipment - - - - io.openliberty.tools - liberty-maven-plugin - 3.8.2 - - shipmentServer - runnable - 120 - - /shipment - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/chapter11/shipment/run-docker.sh b/code/chapter11/chapter11/shipment/run-docker.sh deleted file mode 100644 index 69a51506..00000000 --- a/code/chapter11/chapter11/shipment/run-docker.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Build and run the Shipment Service in Docker -echo "Building and starting Shipment Service in Docker..." - -# Build the application -mvn clean package - -# Build and run the Docker image -docker build -t shipment-service . -docker run -p 8060:8060 -p 9060:9060 --name shipment-service shipment-service diff --git a/code/chapter11/chapter11/shipment/run.sh b/code/chapter11/chapter11/shipment/run.sh deleted file mode 100644 index b6fd34ac..00000000 --- a/code/chapter11/chapter11/shipment/run.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Build and run the Shipment Service -echo "Building and starting Shipment Service..." - -# Stop running server if already running -if [ -f target/liberty/wlp/usr/servers/shipmentServer/workarea/.sRunning ]; then - mvn liberty:stop -fi - -# Clean, build and run -mvn clean package liberty:run diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java deleted file mode 100644 index 3f7288ba..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.microprofile.tutorial.store.shipment; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; -import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; -import org.eclipse.microprofile.openapi.annotations.info.Contact; -import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.info.License; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -/** - * JAX-RS Application class for the shipment service. - */ -@ApplicationPath("/api") -@OpenAPIDefinition( - info = @Info( - title = "Shipment Service API", - version = "1.0.0", - description = "API for managing shipments in the microprofile tutorial store", - contact = @Contact( - name = "Shipment Service Support", - email = "shipment@example.com" - ), - license = @License( - name = "Apache 2.0", - url = "https://www.apache.org/licenses/LICENSE-2.0.html" - ) - ), - tags = { - @Tag(name = "Shipment Resource", description = "Operations for managing shipments") - } -) -public class ShipmentApplication extends Application { - // Empty application class, all configuration is provided by annotations -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java deleted file mode 100644 index ba3ce1f8..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java +++ /dev/null @@ -1,193 +0,0 @@ -package io.microprofile.tutorial.store.shipment.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Order Service. - */ -@ApplicationScoped -public class OrderClient { - - private static final Logger LOGGER = Logger.getLogger(OrderClient.class.getName()); - - @ConfigProperty(name = "order.service.url", defaultValue = "http://localhost:8050/order") - private String orderServiceUrl; - - /** - * Updates the order status after a shipment has been processed. - * - * @param orderId The ID of the order to update - * @param newStatus The new status for the order - * @return true if the update was successful, false otherwise - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "updateOrderStatusFallback") - public boolean updateOrderStatus(Long orderId, String newStatus) { - LOGGER.info(String.format("Updating order %d status to %s", orderId, newStatus)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d/status/%s", orderServiceUrl, orderId, newStatus); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .put(Entity.json("{}")); - - boolean success = response.getStatus() == Response.Status.OK.getStatusCode(); - if (!success) { - LOGGER.warning(String.format("Failed to update order status. Status code: %d", response.getStatus())); - } - return success; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Verifies that an order exists and is in a valid state for shipment. - * - * @param orderId The ID of the order to verify - * @return true if the order exists and is in a valid state, false otherwise - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "verifyOrderFallback") - public boolean verifyOrder(Long orderId) { - LOGGER.info(String.format("Verifying order %d for shipment", orderId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple check if the order is in a valid state for shipment - // In a real app, we'd parse the JSON properly - return jsonResponse.contains("\"status\":\"PAID\"") || - jsonResponse.contains("\"status\":\"PROCESSING\"") || - jsonResponse.contains("\"status\":\"READY_FOR_SHIPMENT\""); - } - - LOGGER.warning(String.format("Failed to verify order. Status code: %d", response.getStatus())); - return false; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Gets the shipping address for an order. - * - * @param orderId The ID of the order - * @return The shipping address, or null if not found - */ - @Retry(maxRetries = 3, delay = 1000, jitter = 200) - @Timeout(value = 5, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - @Fallback(fallbackMethod = "getShippingAddressFallback") - public String getShippingAddress(Long orderId) { - LOGGER.info(String.format("Getting shipping address for order %d", orderId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple extract of shipping address - in real app use proper JSON parsing - if (jsonResponse.contains("\"shippingAddress\":")) { - int startIndex = jsonResponse.indexOf("\"shippingAddress\":") + "\"shippingAddress\":".length(); - startIndex = jsonResponse.indexOf("\"", startIndex) + 1; - int endIndex = jsonResponse.indexOf("\"", startIndex); - if (endIndex > startIndex) { - return jsonResponse.substring(startIndex, endIndex); - } - } - } - - LOGGER.warning(String.format("Failed to get shipping address. Status code: %d", response.getStatus())); - return null; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for updateOrderStatus. - * - * @param orderId The ID of the order - * @param newStatus The new status for the order - * @return false, indicating failure - */ - public boolean updateOrderStatusFallback(Long orderId, String newStatus) { - LOGGER.warning(String.format("Using fallback for order status update. Order ID: %d, Status: %s", orderId, newStatus)); - return false; - } - - /** - * Fallback method for verifyOrder. - * - * @param orderId The ID of the order - * @return false, indicating failure - */ - public boolean verifyOrderFallback(Long orderId) { - LOGGER.warning(String.format("Using fallback for order verification. Order ID: %d", orderId)); - return false; - } - - /** - * Fallback method for getShippingAddress. - * - * @param orderId The ID of the order - * @return null, indicating failure - */ - public String getShippingAddressFallback(Long orderId) { - LOGGER.warning(String.format("Using fallback for getting shipping address. Order ID: %d", orderId)); - return null; - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java deleted file mode 100644 index d9bea89e..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.microprofile.tutorial.store.shipment.entity; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * Shipment class for the microprofile tutorial store application. - * This class represents a shipment of an order in the system. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Shipment { - - private Long shipmentId; - - @NotNull(message = "Order ID cannot be null") - private Long orderId; - - private String trackingNumber; - - @NotNull(message = "Status cannot be null") - private ShipmentStatus status; - - private LocalDateTime estimatedDelivery; - - private LocalDateTime shippedAt; - - @Builder.Default - private LocalDateTime createdAt = LocalDateTime.now(); - - private LocalDateTime updatedAt; - - private String carrier; - - private String shippingAddress; - - private String notes; -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java deleted file mode 100644 index 0e120a9b..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.microprofile.tutorial.store.shipment.entity; - -/** - * ShipmentStatus enum for the microprofile tutorial store application. - * This enum defines the possible statuses for a shipment. - */ -public enum ShipmentStatus { - PENDING, // Shipment is pending - PROCESSING, // Shipment is being processed - SHIPPED, // Shipment has been shipped - IN_TRANSIT, // Shipment is in transit - OUT_FOR_DELIVERY,// Shipment is out for delivery - DELIVERED, // Shipment has been delivered - FAILED, // Shipment delivery failed - RETURNED // Shipment was returned -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java deleted file mode 100644 index ec26495e..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.microprofile.tutorial.store.shipment.filter; - -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Filter to enable CORS for the Shipment service. - */ -public class CorsFilter implements Filter { - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // No initialization required - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - - // Allow requests from any origin - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); - response.setHeader("Access-Control-Max-Age", "3600"); - - // For preflight requests - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { - response.setStatus(HttpServletResponse.SC_OK); - } else { - chain.doFilter(request, response); - } - } - - @Override - public void destroy() { - // No cleanup required - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java deleted file mode 100644 index 4bf8a503..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.microprofile.tutorial.store.shipment.health; - -import io.microprofile.tutorial.store.shipment.client.OrderClient; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.health.HealthCheck; -import org.eclipse.microprofile.health.HealthCheckResponse; -import org.eclipse.microprofile.health.Liveness; -import org.eclipse.microprofile.health.Readiness; - -/** - * Health check for the shipment service. - */ -@ApplicationScoped -public class ShipmentHealthCheck { - - @Inject - private OrderClient orderClient; - - /** - * Liveness check for the shipment service. - * Verifies that the application is running and not in a failed state. - * - * @return HealthCheckResponse indicating whether the service is live - */ - @Liveness - @ApplicationScoped - public static class LivenessCheck implements HealthCheck { - @Override - public HealthCheckResponse call() { - return HealthCheckResponse.named("shipment-liveness") - .up() - .withData("memory", Runtime.getRuntime().freeMemory()) - .build(); - } - } - - /** - * Readiness check for the shipment service. - * Verifies that the service is ready to handle requests, including connectivity to dependencies. - * - * @return HealthCheckResponse indicating whether the service is ready - */ - @Readiness - @ApplicationScoped - public class ReadinessCheck implements HealthCheck { - @Override - public HealthCheckResponse call() { - boolean orderServiceReachable = false; - - try { - // Simple check to see if the Order service is reachable - // We use a dummy order ID just to test connectivity - orderClient.getShippingAddress(999999L); - orderServiceReachable = true; - } catch (Exception e) { - // If the order service is not reachable, the health check will fail - orderServiceReachable = false; - } - - return HealthCheckResponse.named("shipment-readiness") - .status(orderServiceReachable) - .withData("orderServiceReachable", orderServiceReachable) - .build(); - } - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java deleted file mode 100644 index c4013a92..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.microprofile.tutorial.store.shipment.repository; - -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Simple in-memory repository for Shipment objects. - * This class provides CRUD operations for Shipment entities. - */ -@ApplicationScoped -public class ShipmentRepository { - - private final Map shipments = new ConcurrentHashMap<>(); - private long nextId = 1; - - /** - * Saves a shipment to the repository. - * If the shipment has no ID, a new ID is assigned. - * - * @param shipment The shipment to save - * @return The saved shipment with ID assigned - */ - public Shipment save(Shipment shipment) { - if (shipment.getShipmentId() == null) { - shipment.setShipmentId(nextId++); - } - - if (shipment.getCreatedAt() == null) { - shipment.setCreatedAt(LocalDateTime.now()); - } - - shipment.setUpdatedAt(LocalDateTime.now()); - - shipments.put(shipment.getShipmentId(), shipment); - return shipment; - } - - /** - * Finds a shipment by ID. - * - * @param id The shipment ID - * @return An Optional containing the shipment if found, or empty if not found - */ - public Optional findById(Long id) { - return Optional.ofNullable(shipments.get(id)); - } - - /** - * Finds shipments by order ID. - * - * @param orderId The order ID - * @return A list of shipments for the specified order - */ - public List findByOrderId(Long orderId) { - return shipments.values().stream() - .filter(shipment -> shipment.getOrderId().equals(orderId)) - .collect(Collectors.toList()); - } - - /** - * Finds shipments by tracking number. - * - * @param trackingNumber The tracking number - * @return A list of shipments with the specified tracking number - */ - public List findByTrackingNumber(String trackingNumber) { - return shipments.values().stream() - .filter(shipment -> trackingNumber.equals(shipment.getTrackingNumber())) - .collect(Collectors.toList()); - } - - /** - * Finds shipments by status. - * - * @param status The shipment status - * @return A list of shipments with the specified status - */ - public List findByStatus(ShipmentStatus status) { - return shipments.values().stream() - .filter(shipment -> shipment.getStatus() == status) - .collect(Collectors.toList()); - } - - /** - * Finds shipments that are expected to be delivered by a certain date. - * - * @param deliveryDate The delivery date - * @return A list of shipments expected to be delivered by the specified date - */ - public List findByEstimatedDeliveryBefore(LocalDateTime deliveryDate) { - return shipments.values().stream() - .filter(shipment -> shipment.getEstimatedDelivery() != null && - shipment.getEstimatedDelivery().isBefore(deliveryDate)) - .collect(Collectors.toList()); - } - - /** - * Retrieves all shipments from the repository. - * - * @return A list of all shipments - */ - public List findAll() { - return new ArrayList<>(shipments.values()); - } - - /** - * Deletes a shipment by ID. - * - * @param id The ID of the shipment to delete - * @return true if the shipment was deleted, false if not found - */ - public boolean deleteById(Long id) { - return shipments.remove(id) != null; - } - - /** - * Updates an existing shipment. - * - * @param id The ID of the shipment to update - * @param shipment The updated shipment information - * @return An Optional containing the updated shipment, or empty if not found - */ - public Optional update(Long id, Shipment shipment) { - if (!shipments.containsKey(id)) { - return Optional.empty(); - } - - // Preserve creation date - LocalDateTime createdAt = shipments.get(id).getCreatedAt(); - shipment.setCreatedAt(createdAt); - - shipment.setShipmentId(id); - shipment.setUpdatedAt(LocalDateTime.now()); - - shipments.put(id, shipment); - return Optional.of(shipment); - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java deleted file mode 100644 index 602be808..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java +++ /dev/null @@ -1,397 +0,0 @@ -package io.microprofile.tutorial.store.shipment.resource; - -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; -import io.microprofile.tutorial.store.shipment.service.ShipmentService; -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * REST resource for shipment operations. - */ -@Path("/api/shipments") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Shipment Resource", description = "Operations for managing shipments") -public class ShipmentResource { - - private static final Logger LOGGER = Logger.getLogger(ShipmentResource.class.getName()); - - @Inject - private ShipmentService shipmentService; - - /** - * Creates a new shipment for an order. - * - * @param orderId The order ID - * @return The created shipment - */ - @POST - @Path("/orders/{orderId}") - @Operation(summary = "Create a new shipment for an order") - @APIResponse(responseCode = "201", description = "Shipment created", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "400", description = "Invalid order ID") - @APIResponse(responseCode = "404", description = "Order not found or not ready for shipment") - public Response createShipment( - @Parameter(description = "Order ID", required = true) - @PathParam("orderId") Long orderId) { - - LOGGER.info("REST request to create shipment for order: " + orderId); - - Shipment shipment = shipmentService.createShipment(orderId); - if (shipment == null) { - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Order not found or not ready for shipment\"}") - .build(); - } - - return Response.status(Response.Status.CREATED) - .entity(shipment) - .build(); - } - - /** - * Gets a shipment by ID. - * - * @param shipmentId The shipment ID - * @return The shipment - */ - @GET - @Path("/{shipmentId}") - @Operation(summary = "Get a shipment by ID") - @APIResponse(responseCode = "200", description = "Shipment found", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response getShipment( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId) { - - LOGGER.info("REST request to get shipment: " + shipmentId); - - Optional shipment = shipmentService.getShipment(shipmentId); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Gets all shipments. - * - * @return All shipments - */ - @GET - @Operation(summary = "Get all shipments") - @APIResponse(responseCode = "200", description = "All shipments", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getAllShipments() { - LOGGER.info("REST request to get all shipments"); - - List shipments = shipmentService.getAllShipments(); - return Response.ok(shipments).build(); - } - - /** - * Gets shipments by status. - * - * @param status The status - * @return The shipments with the given status - */ - @GET - @Path("/status/{status}") - @Operation(summary = "Get shipments by status") - @APIResponse(responseCode = "200", description = "Shipments with the given status", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getShipmentsByStatus( - @Parameter(description = "Shipment status", required = true) - @PathParam("status") ShipmentStatus status) { - - LOGGER.info("REST request to get shipments with status: " + status); - - List shipments = shipmentService.getShipmentsByStatus(status); - return Response.ok(shipments).build(); - } - - /** - * Gets shipments by order ID. - * - * @param orderId The order ID - * @return The shipments for the given order - */ - @GET - @Path("/orders/{orderId}") - @Operation(summary = "Get shipments by order ID") - @APIResponse(responseCode = "200", description = "Shipments for the given order", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) - public Response getShipmentsByOrder( - @Parameter(description = "Order ID", required = true) - @PathParam("orderId") Long orderId) { - - LOGGER.info("REST request to get shipments for order: " + orderId); - - List shipments = shipmentService.getShipmentsByOrder(orderId); - return Response.ok(shipments).build(); - } - - /** - * Gets a shipment by tracking number. - * - * @param trackingNumber The tracking number - * @return The shipment - */ - @GET - @Path("/tracking/{trackingNumber}") - @Operation(summary = "Get a shipment by tracking number") - @APIResponse(responseCode = "200", description = "Shipment found", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response getShipmentByTrackingNumber( - @Parameter(description = "Tracking number", required = true) - @PathParam("trackingNumber") String trackingNumber) { - - LOGGER.info("REST request to get shipment with tracking number: " + trackingNumber); - - Optional shipment = shipmentService.getShipmentByTrackingNumber(trackingNumber); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the status of a shipment. - * - * @param shipmentId The shipment ID - * @param status The new status - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/status/{status}") - @Operation(summary = "Update shipment status") - @APIResponse(responseCode = "200", description = "Shipment status updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateShipmentStatus( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New status", required = true) - @PathParam("status") ShipmentStatus status) { - - LOGGER.info("REST request to update shipment " + shipmentId + " status to " + status); - - Optional shipment = shipmentService.updateShipmentStatus(shipmentId, status); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the carrier for a shipment. - * - * @param shipmentId The shipment ID - * @param carrier The new carrier - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/carrier") - @Operation(summary = "Update shipment carrier") - @APIResponse(responseCode = "200", description = "Carrier updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateCarrier( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New carrier", required = true) - @NotNull String carrier) { - - LOGGER.info("REST request to update carrier for shipment " + shipmentId + " to " + carrier); - - Optional shipment = shipmentService.updateCarrier(shipmentId, carrier); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the tracking number for a shipment. - * - * @param shipmentId The shipment ID - * @param trackingNumber The new tracking number - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/tracking") - @Operation(summary = "Update shipment tracking number") - @APIResponse(responseCode = "200", description = "Tracking number updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateTrackingNumber( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New tracking number", required = true) - @NotNull String trackingNumber) { - - LOGGER.info("REST request to update tracking number for shipment " + shipmentId + " to " + trackingNumber); - - Optional shipment = shipmentService.updateTrackingNumber(shipmentId, trackingNumber); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Updates the estimated delivery date for a shipment. - * - * @param shipmentId The shipment ID - * @param dateStr The new estimated delivery date (ISO format) - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/delivery-date") - @Operation(summary = "Update shipment estimated delivery date") - @APIResponse(responseCode = "200", description = "Estimated delivery date updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "400", description = "Invalid date format") - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateEstimatedDelivery( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New estimated delivery date (ISO format: yyyy-MM-dd'T'HH:mm:ss)", required = true) - @NotNull String dateStr) { - - LOGGER.info("REST request to update estimated delivery for shipment " + shipmentId + " to " + dateStr); - - try { - LocalDateTime date = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); - Optional shipment = shipmentService.updateEstimatedDelivery(shipmentId, date); - - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } catch (Exception e) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Invalid date format. Use ISO format: yyyy-MM-dd'T'HH:mm:ss\"}") - .build(); - } - } - - /** - * Updates the notes for a shipment. - * - * @param shipmentId The shipment ID - * @param notes The new notes - * @return The updated shipment - */ - @PUT - @Path("/{shipmentId}/notes") - @Operation(summary = "Update shipment notes") - @APIResponse(responseCode = "200", description = "Notes updated", - content = @Content(mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = Shipment.class))) - @APIResponse(responseCode = "404", description = "Shipment not found") - public Response updateNotes( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId, - @Parameter(description = "New notes", required = true) - String notes) { - - LOGGER.info("REST request to update notes for shipment " + shipmentId); - - Optional shipment = shipmentService.updateNotes(shipmentId, notes); - if (shipment.isPresent()) { - return Response.ok(shipment.get()).build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } - - /** - * Deletes a shipment. - * - * @param shipmentId The shipment ID - * @return A response indicating success or failure - */ - @DELETE - @Path("/{shipmentId}") - @Operation(summary = "Delete a shipment") - @APIResponse(responseCode = "204", description = "Shipment deleted") - @APIResponse(responseCode = "404", description = "Shipment not found") - @APIResponse(responseCode = "400", description = "Shipment cannot be deleted due to its status") - public Response deleteShipment( - @Parameter(description = "Shipment ID", required = true) - @PathParam("shipmentId") Long shipmentId) { - - LOGGER.info("REST request to delete shipment: " + shipmentId); - - boolean deleted = shipmentService.deleteShipment(shipmentId); - if (deleted) { - return Response.noContent().build(); - } - - // Check if shipment exists but cannot be deleted due to its status - Optional shipment = shipmentService.getShipment(shipmentId); - if (shipment.isPresent()) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Shipment cannot be deleted due to its status\"}") - .build(); - } - - return Response.status(Response.Status.NOT_FOUND) - .entity("{\"error\": \"Shipment not found\"}") - .build(); - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java b/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java deleted file mode 100644 index f29aadee..00000000 --- a/code/chapter11/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java +++ /dev/null @@ -1,305 +0,0 @@ -package io.microprofile.tutorial.store.shipment.service; - -import io.microprofile.tutorial.store.shipment.client.OrderClient; -import io.microprofile.tutorial.store.shipment.entity.Shipment; -import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; -import io.microprofile.tutorial.store.shipment.repository.ShipmentRepository; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.metrics.annotation.Counted; -import org.eclipse.microprofile.metrics.annotation.Timed; - -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.logging.Logger; - -/** - * Shipment Service for managing shipments. - */ -@ApplicationScoped -public class ShipmentService { - - private static final Logger LOGGER = Logger.getLogger(ShipmentService.class.getName()); - private static final Random RANDOM = new Random(); - private static final String[] CARRIERS = {"FedEx", "UPS", "USPS", "DHL", "Amazon Logistics"}; - - @Inject - private ShipmentRepository shipmentRepository; - - @Inject - private OrderClient orderClient; - - /** - * Creates a new shipment for an order. - * - * @param orderId The order ID - * @return The created shipment, or null if the order is invalid - */ - @Counted(name = "shipmentCreations", description = "Number of shipments created") - @Timed(name = "createShipmentTimer", description = "Time to create a shipment") - public Shipment createShipment(Long orderId) { - LOGGER.info("Creating shipment for order: " + orderId); - - // Verify that the order exists and is ready for shipment - if (!orderClient.verifyOrder(orderId)) { - LOGGER.warning("Order " + orderId + " is not valid for shipment"); - return null; - } - - // Get shipping address from order service - String shippingAddress = orderClient.getShippingAddress(orderId); - if (shippingAddress == null) { - LOGGER.warning("Could not retrieve shipping address for order " + orderId); - return null; - } - - // Create a new shipment - Shipment shipment = Shipment.builder() - .orderId(orderId) - .status(ShipmentStatus.PENDING) - .trackingNumber(generateTrackingNumber()) - .carrier(selectRandomCarrier()) - .shippingAddress(shippingAddress) - .estimatedDelivery(LocalDateTime.now().plusDays(5)) - .createdAt(LocalDateTime.now()) - .build(); - - Shipment savedShipment = shipmentRepository.save(shipment); - - // Update order status to indicate shipment is being processed - orderClient.updateOrderStatus(orderId, "SHIPMENT_CREATED"); - - return savedShipment; - } - - /** - * Updates the status of a shipment. - * - * @param shipmentId The shipment ID - * @param status The new status - * @return The updated shipment, or empty if not found - */ - @Counted(name = "shipmentStatusUpdates", description = "Number of shipment status updates") - public Optional updateShipmentStatus(Long shipmentId, ShipmentStatus status) { - LOGGER.info("Updating shipment " + shipmentId + " status to " + status); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setStatus(status); - shipment.setUpdatedAt(LocalDateTime.now()); - - // If status is SHIPPED, set the shipped date - if (status == ShipmentStatus.SHIPPED) { - shipment.setShippedAt(LocalDateTime.now()); - orderClient.updateOrderStatus(shipment.getOrderId(), "SHIPPED"); - } - // If status is DELIVERED, update order status - else if (status == ShipmentStatus.DELIVERED) { - orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERED"); - } - // If status is FAILED, update order status - else if (status == ShipmentStatus.FAILED) { - orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERY_FAILED"); - } - - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Gets a shipment by ID. - * - * @param shipmentId The shipment ID - * @return The shipment, or empty if not found - */ - public Optional getShipment(Long shipmentId) { - LOGGER.info("Getting shipment: " + shipmentId); - return shipmentRepository.findById(shipmentId); - } - - /** - * Gets all shipments for an order. - * - * @param orderId The order ID - * @return The list of shipments for the order - */ - public List getShipmentsByOrder(Long orderId) { - LOGGER.info("Getting shipments for order: " + orderId); - return shipmentRepository.findByOrderId(orderId); - } - - /** - * Gets a shipment by tracking number. - * - * @param trackingNumber The tracking number - * @return The shipment, or empty if not found - */ - public Optional getShipmentByTrackingNumber(String trackingNumber) { - LOGGER.info("Getting shipment with tracking number: " + trackingNumber); - List shipments = shipmentRepository.findByTrackingNumber(trackingNumber); - return shipments.isEmpty() ? Optional.empty() : Optional.of(shipments.get(0)); - } - - /** - * Gets all shipments. - * - * @return All shipments - */ - public List getAllShipments() { - LOGGER.info("Getting all shipments"); - return shipmentRepository.findAll(); - } - - /** - * Gets shipments by status. - * - * @param status The status - * @return The list of shipments with the given status - */ - public List getShipmentsByStatus(ShipmentStatus status) { - LOGGER.info("Getting shipments with status: " + status); - return shipmentRepository.findByStatus(status); - } - - /** - * Gets shipments due for delivery by the given date. - * - * @param date The date - * @return The list of shipments due by the given date - */ - public List getShipmentsDueBy(LocalDateTime date) { - LOGGER.info("Getting shipments due by: " + date); - return shipmentRepository.findByEstimatedDeliveryBefore(date); - } - - /** - * Updates the carrier for a shipment. - * - * @param shipmentId The shipment ID - * @param carrier The new carrier - * @return The updated shipment, or empty if not found - */ - public Optional updateCarrier(Long shipmentId, String carrier) { - LOGGER.info("Updating carrier for shipment " + shipmentId + " to " + carrier); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setCarrier(carrier); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the tracking number for a shipment. - * - * @param shipmentId The shipment ID - * @param trackingNumber The new tracking number - * @return The updated shipment, or empty if not found - */ - public Optional updateTrackingNumber(Long shipmentId, String trackingNumber) { - LOGGER.info("Updating tracking number for shipment " + shipmentId + " to " + trackingNumber); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setTrackingNumber(trackingNumber); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the estimated delivery date for a shipment. - * - * @param shipmentId The shipment ID - * @param estimatedDelivery The new estimated delivery date - * @return The updated shipment, or empty if not found - */ - public Optional updateEstimatedDelivery(Long shipmentId, LocalDateTime estimatedDelivery) { - LOGGER.info("Updating estimated delivery for shipment " + shipmentId + " to " + estimatedDelivery); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setEstimatedDelivery(estimatedDelivery); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Updates the notes for a shipment. - * - * @param shipmentId The shipment ID - * @param notes The new notes - * @return The updated shipment, or empty if not found - */ - public Optional updateNotes(Long shipmentId, String notes) { - LOGGER.info("Updating notes for shipment " + shipmentId); - - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - Shipment shipment = shipmentOpt.get(); - shipment.setNotes(notes); - shipment.setUpdatedAt(LocalDateTime.now()); - return Optional.of(shipmentRepository.save(shipment)); - } - - return Optional.empty(); - } - - /** - * Deletes a shipment. - * - * @param shipmentId The shipment ID - * @return true if the shipment was deleted, false if not found - */ - public boolean deleteShipment(Long shipmentId) { - LOGGER.info("Deleting shipment: " + shipmentId); - Optional shipmentOpt = shipmentRepository.findById(shipmentId); - if (shipmentOpt.isPresent()) { - // Only allow deletion if the shipment is in PENDING or PROCESSING status - ShipmentStatus status = shipmentOpt.get().getStatus(); - if (status == ShipmentStatus.PENDING || status == ShipmentStatus.PROCESSING) { - return shipmentRepository.deleteById(shipmentId); - } - LOGGER.warning("Cannot delete shipment with status: " + status); - return false; - } - return false; - } - - /** - * Generates a random tracking number. - * - * @return A random tracking number - */ - private String generateTrackingNumber() { - return String.format("%s-%04d-%04d-%04d", - CARRIERS[RANDOM.nextInt(CARRIERS.length)].substring(0, 2).toUpperCase(), - RANDOM.nextInt(10000), - RANDOM.nextInt(10000), - RANDOM.nextInt(10000)); - } - - /** - * Selects a random carrier. - * - * @return A random carrier - */ - private String selectRandomCarrier() { - return CARRIERS[RANDOM.nextInt(CARRIERS.length)]; - } -} diff --git a/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index 5057c129..00000000 --- a/code/chapter11/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,32 +0,0 @@ -# Shipment Service Configuration - -# Order Service URL -order.service.url=http://localhost:8050/order - -# Configure health check properties -mp.health.check.timeout=5s - -# Configure default MP Metrics properties -mp.metrics.tags=app=shipment-service - -# Configure fault tolerance policies -# Retry configuration -mp.fault.tolerance.Retry.delay=1000 -mp.fault.tolerance.Retry.maxRetries=3 -mp.fault.tolerance.Retry.jitter=200 - -# Timeout configuration -mp.fault.tolerance.Timeout.value=5000 - -# Circuit Breaker configuration -mp.fault.tolerance.CircuitBreaker.requestVolumeThreshold=5 -mp.fault.tolerance.CircuitBreaker.failureRatio=0.5 -mp.fault.tolerance.CircuitBreaker.delay=10000 -mp.fault.tolerance.CircuitBreaker.successThreshold=2 - -# Open API configuration -mp.openapi.scan.disable=false -mp.openapi.scan.packages=io.microprofile.tutorial.store.shipment - -# In Docker environment, override the Order service URL -%docker.order.service.url=http://order:8050/order diff --git a/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index ed5b091b..00000000 --- a/code/chapter11/chapter11/shipment/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - Shipment Service - - - index.html - - - diff --git a/code/chapter11/chapter11/shipment/src/main/webapp/index.html b/code/chapter11/chapter11/shipment/src/main/webapp/index.html deleted file mode 100644 index 5641acb7..00000000 --- a/code/chapter11/chapter11/shipment/src/main/webapp/index.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - Shipment Service - MicroProfile Tutorial - - - -

Shipment Service

-

- This is the Shipment Service for the MicroProfile Tutorial e-commerce application. - The service manages shipments for orders in the system. -

- -

REST API

-

- The service exposes the following endpoints: -

- -
-

POST /api/shipments/orders/{orderId}

-

Create a new shipment for an order.

-
- -
-

GET /api/shipments/{shipmentId}

-

Get a shipment by ID.

-
- -
-

GET /api/shipments

-

Get all shipments.

-
- -
-

GET /api/shipments/status/{status}

-

Get shipments by status (e.g., PENDING, PROCESSING, SHIPPED, etc.).

-
- -
-

GET /api/shipments/orders/{orderId}

-

Get all shipments for an order.

-
- -
-

GET /api/shipments/tracking/{trackingNumber}

-

Get a shipment by tracking number.

-
- -
-

PUT /api/shipments/{shipmentId}/status/{status}

-

Update the status of a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/carrier

-

Update the carrier for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/tracking

-

Update the tracking number for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/delivery-date

-

Update the estimated delivery date for a shipment.

-
- -
-

PUT /api/shipments/{shipmentId}/notes

-

Update the notes for a shipment.

-
- -
-

DELETE /api/shipments/{shipmentId}

-

Delete a shipment (only allowed for shipments in PENDING or PROCESSING status).

-
- -

OpenAPI Documentation

-

- The service provides OpenAPI documentation at /shipment/openapi. - You can also access the Swagger UI at /shipment/openapi/ui. -

- -

Health Checks

-

- MicroProfile Health endpoints are available at: -

- - -

Metrics

-

- MicroProfile Metrics are available at /shipment/metrics. -

- -
-

Shipment Service - MicroProfile Tutorial E-commerce Application

-
- - diff --git a/code/chapter11/chapter11/shoppingcart/Dockerfile b/code/chapter11/chapter11/shoppingcart/Dockerfile deleted file mode 100644 index c207b409..00000000 --- a/code/chapter11/chapter11/shoppingcart/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi - -# Copy configuration files -COPY --chown=1001:0 src/main/liberty/config/ /config/ - -# Create the apps directory and copy the application -COPY --chown=1001:0 target/shoppingcart.war /config/apps/ - -# Configure the server to run in production mode -RUN configure.sh - -# Expose the default port -EXPOSE 4050 4443 - -# Set the health check -HEALTHCHECK --start-period=60s --interval=10s --timeout=5s --retries=3 \ - CMD curl -f http://localhost:4050/health || exit 1 - -# Run the server -CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/chapter11/shoppingcart/README.md b/code/chapter11/chapter11/shoppingcart/README.md deleted file mode 100644 index a989bfe8..00000000 --- a/code/chapter11/chapter11/shoppingcart/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Shopping Cart Service - -This microservice is part of the Jakarta EE and MicroProfile-based e-commerce application. It handles shopping cart management for users. - -## Features - -- Create and manage user shopping carts -- Add products to cart with quantity -- Update and remove cart items -- Check product availability via the Inventory Service -- Fetch product details from the Catalog Service - -## Endpoints - -### GET /shoppingcart/api/carts -- Returns all shopping carts in the system - -### GET /shoppingcart/api/carts/{id} -- Returns a specific shopping cart by ID - -### GET /shoppingcart/api/carts/user/{userId} -- Returns or creates a shopping cart for a specific user - -### POST /shoppingcart/api/carts/user/{userId} -- Creates a new shopping cart for a user - -### POST /shoppingcart/api/carts/{cartId}/items -- Adds an item to a shopping cart -- Request body: CartItem JSON - -### PUT /shoppingcart/api/carts/{cartId}/items/{itemId} -- Updates an item in a shopping cart -- Request body: Updated CartItem JSON - -### DELETE /shoppingcart/api/carts/{cartId}/items/{itemId} -- Removes an item from a shopping cart - -### DELETE /shoppingcart/api/carts/{cartId}/items -- Removes all items from a shopping cart - -### DELETE /shoppingcart/api/carts/{cartId} -- Deletes a shopping cart - -## Cart Item JSON Example - -```json -{ - "productId": 1, - "quantity": 2, - "productName": "Product Name", // Optional, will be fetched from Catalog if not provided - "price": 29.99, // Optional, will be fetched from Catalog if not provided - "imageUrl": "product-image.jpg" // Optional, will be fetched from Catalog if not provided -} -``` - -## Running the Service - -### Local Development - -```bash -./run.sh -``` - -### Docker - -```bash -./run-docker.sh -``` - -## Integration with Other Services - -The Shopping Cart Service integrates with: - -- **Inventory Service**: Checks product availability before adding to cart -- **Catalog Service**: Retrieves product details (name, price, image) -- **Order Service**: Indirectly, when a cart is converted to an order - -## MicroProfile Features Used - -- **Config**: For service URL configuration -- **Fault Tolerance**: Circuit breakers, timeouts, retries, and fallbacks for resilient communication -- **Health**: Liveness and readiness checks -- **OpenAPI**: API documentation - -## Swagger UI - -OpenAPI documentation is available at: `http://localhost:4050/shoppingcart/api/openapi-ui/` diff --git a/code/chapter11/chapter11/shoppingcart/pom.xml b/code/chapter11/chapter11/shoppingcart/pom.xml deleted file mode 100644 index df9eae09..00000000 --- a/code/chapter11/chapter11/shoppingcart/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - io.microprofile - shoppingcart - 1.0-SNAPSHOT - war - - shopping-cart-service - https://microprofile.io - - - UTF-8 - 21 - 10.0.0 - 7.1 - 25.0.0.1 - 1.18.36 - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakarta.jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile.version} - pom - provided - - - - org.projectlombok - lombok - ${lombok.version} - provided - - - - - shoppingcart - - - - io.openliberty.tools - liberty-maven-plugin - 3.8.2 - - shoppingcartServer - runnable - 120 - - /shoppingcart - - - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-war-plugin - 3.3.2 - - false - - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - diff --git a/code/chapter11/chapter11/shoppingcart/run-docker.sh b/code/chapter11/chapter11/shoppingcart/run-docker.sh deleted file mode 100644 index 6b32df82..00000000 --- a/code/chapter11/chapter11/shoppingcart/run-docker.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Script to build and run the Shopping Cart service in Docker - -# Stop execution on any error -set -e - -# Navigate to the shopping cart service directory -cd "$(dirname "$0")" - -# Build the project with Maven -echo "Building with Maven..." -mvn clean package - -# Build the Docker image -echo "Building Docker image..." -docker build -t shoppingcart-service . - -# Run the Docker container -echo "Starting Docker container..." -docker run -d --name shoppingcart-service -p 4050:4050 shoppingcart-service - -echo "Shopping Cart service is running on http://localhost:4050/shoppingcart" diff --git a/code/chapter11/chapter11/shoppingcart/run.sh b/code/chapter11/chapter11/shoppingcart/run.sh deleted file mode 100644 index 02b3ee65..00000000 --- a/code/chapter11/chapter11/shoppingcart/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Script to build and run the Shopping Cart service - -# Stop execution on any error -set -e - -echo "Building and running Shopping Cart service..." - -# Navigate to the shopping cart service directory -cd "$(dirname "$0")" - -# Build the project with Maven -echo "Building with Maven..." -mvn clean package - -# Run the application using Liberty Maven plugin -echo "Starting Liberty server..." -mvn liberty:run diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java deleted file mode 100644 index 84cfe0d6..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart; - -import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.core.Application; - -/** - * REST application for shopping cart management. - */ -@ApplicationPath("/api") -public class ShoppingCartApplication extends Application { - // The resources will be discovered automatically -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java deleted file mode 100644 index e13684cc..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Catalog Service. - */ -@ApplicationScoped -public class CatalogClient { - - private static final Logger LOGGER = Logger.getLogger(CatalogClient.class.getName()); - - @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog") - private String catalogServiceUrl; - - // Cache for product details to reduce service calls - private final Map productCache = new HashMap<>(); - - /** - * Gets product information from the catalog service. - * - * @param productId The product ID - * @return ProductInfo containing product details - */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "getProductInfoFallback") - public ProductInfo getProductInfo(Long productId) { - // Check cache first - if (productCache.containsKey(productId)) { - return productCache.get(productId); - } - - LOGGER.info(String.format("Fetching product info for product %d", productId)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/products/%d", catalogServiceUrl, productId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple parsing - in a real app, use proper JSON parsing - String name = extractField(jsonResponse, "name"); - String priceStr = extractField(jsonResponse, "price"); - - double price = 0.0; - try { - price = Double.parseDouble(priceStr); - } catch (NumberFormatException e) { - LOGGER.warning("Failed to parse product price: " + priceStr); - } - - ProductInfo productInfo = new ProductInfo(productId, name, price); - - // Cache the result - productCache.put(productId, productInfo); - - return productInfo; - } - - LOGGER.warning(String.format("Failed to get product info. Status code: %d", response.getStatus())); - return new ProductInfo(productId, "Unknown Product", 0.0); - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Catalog Service", e); - throw e; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for getProductInfo. - * Returns a placeholder product info when the catalog service is unavailable. - * - * @param productId The product ID - * @return A placeholder ProductInfo object - */ - public ProductInfo getProductInfoFallback(Long productId) { - LOGGER.warning(String.format("Using fallback for product info. Product ID: %d", productId)); - - // Check if we have a cached version - if (productCache.containsKey(productId)) { - return productCache.get(productId); - } - - // Return a placeholder - return new ProductInfo( - productId, - "Product " + productId + " (Service Unavailable)", - 0.0 - ); - } - - /** - * Helper method to extract field values from JSON string. - * This is a simplified approach - in a real app, use a proper JSON parser. - * - * @param jsonString The JSON string - * @param fieldName The name of the field to extract - * @return The extracted field value - */ - private String extractField(String jsonString, String fieldName) { - String searchPattern = "\"" + fieldName + "\":"; - if (jsonString.contains(searchPattern)) { - int startIndex = jsonString.indexOf(searchPattern) + searchPattern.length(); - int endIndex; - - // Skip whitespace - while (startIndex < jsonString.length() && - (jsonString.charAt(startIndex) == ' ' || jsonString.charAt(startIndex) == '\t')) { - startIndex++; - } - - if (startIndex < jsonString.length() && jsonString.charAt(startIndex) == '"') { - // String value - startIndex++; // Skip opening quote - endIndex = jsonString.indexOf("\"", startIndex); - } else { - // Number or boolean value - endIndex = jsonString.indexOf(",", startIndex); - if (endIndex == -1) { - endIndex = jsonString.indexOf("}", startIndex); - } - } - - if (endIndex > startIndex) { - return jsonString.substring(startIndex, endIndex); - } - } - return ""; - } - - /** - * Inner class to hold product information. - */ - public static class ProductInfo { - private final Long productId; - private final String name; - private final double price; - - public ProductInfo(Long productId, String name, double price) { - this.productId = productId; - this.name = name; - this.price = price; - } - - public Long getProductId() { - return productId; - } - - public String getName() { - return name; - } - - public double getPrice() { - return price; - } - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java deleted file mode 100644 index b9ac4c04..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.client; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; - -import java.time.temporal.ChronoUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Client for communicating with the Inventory Service. - */ -@ApplicationScoped -public class InventoryClient { - - private static final Logger LOGGER = Logger.getLogger(InventoryClient.class.getName()); - - @ConfigProperty(name = "inventory.service.url", defaultValue = "http://localhost:7050/inventory") - private String inventoryServiceUrl; - - /** - * Checks if a product is available in sufficient quantity. - * - * @param productId The product ID - * @param quantity The requested quantity - * @return true if the product is available in the requested quantity, false otherwise - */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "checkProductAvailabilityFallback") - public boolean checkProductAvailability(Long productId, int quantity) { - LOGGER.info(String.format("Checking availability for product %d, quantity %d", productId, quantity)); - - Client client = null; - try { - client = ClientBuilder.newClient(); - String url = String.format("%s/api/inventories/product/%d", inventoryServiceUrl, productId); - - Response response = client.target(url) - .request(MediaType.APPLICATION_JSON) - .get(); - - if (response.getStatus() == Response.Status.OK.getStatusCode()) { - String jsonResponse = response.readEntity(String.class); - // Simple parsing - in a real app, use proper JSON parsing - if (jsonResponse.contains("\"quantity\":")) { - String quantityStr = jsonResponse.split("\"quantity\":")[1].split(",")[0].trim(); - int availableQuantity = Integer.parseInt(quantityStr); - return availableQuantity >= quantity; - } - } - - LOGGER.warning(String.format("Failed to check product availability. Status code: %d", response.getStatus())); - return false; - } catch (ProcessingException e) { - LOGGER.log(Level.SEVERE, "Error connecting to Inventory Service", e); - throw e; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error parsing inventory response", e); - return false; - } finally { - if (client != null) { - client.close(); - } - } - } - - /** - * Fallback method for checkProductAvailability. - * Always returns true to allow the cart operation to continue, - * but logs a warning. - * - * @param productId The product ID - * @param quantity The requested quantity - * @return true, allowing the operation to proceed - */ - public boolean checkProductAvailabilityFallback(Long productId, int quantity) { - LOGGER.warning(String.format( - "Using fallback for product availability check. Product ID: %d, Quantity: %d", - productId, quantity)); - // In a production system, you might want to cache product availability - // or implement a more sophisticated fallback mechanism - return true; // Allow the operation to proceed - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java deleted file mode 100644 index dc4537ed..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.entity; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * CartItem class for the microprofile tutorial store application. - * This class represents an item in a shopping cart. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class CartItem { - - private Long itemId; - - @NotNull(message = "Product ID cannot be null") - private Long productId; - - private String productName; - - @Min(value = 0, message = "Price must be greater than or equal to 0") - private double price; - - @Min(value = 1, message = "Quantity must be at least 1") - private int quantity; -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java deleted file mode 100644 index 08f1c0a8..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.entity; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * ShoppingCart class for the microprofile tutorial store application. - * This class represents a user's shopping cart. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ShoppingCart { - - private Long cartId; - - @NotNull(message = "User ID cannot be null") - private Long userId; - - @Builder.Default - private List items = new ArrayList<>(); - - @Builder.Default - private LocalDateTime createdAt = LocalDateTime.now(); - - private LocalDateTime updatedAt; - - /** - * Calculate the total number of items in the cart. - * - * @return The total number of items - */ - public int getTotalItems() { - return items.stream() - .mapToInt(CartItem::getQuantity) - .sum(); - } - - /** - * Calculate the total price of all items in the cart. - * - * @return The total price - */ - public double getTotalPrice() { - return items.stream() - .mapToDouble(item -> item.getPrice() * item.getQuantity()) - .sum(); - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java deleted file mode 100644 index 91dc8339..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java +++ /dev/null @@ -1,68 +0,0 @@ -// package io.microprofile.tutorial.store.shoppingcart.health; - -// import jakarta.enterprise.context.ApplicationScoped; -// import jakarta.inject.Inject; - -// import org.eclipse.microprofile.health.HealthCheck; -// import org.eclipse.microprofile.health.HealthCheckResponse; -// import org.eclipse.microprofile.health.Liveness; -// import org.eclipse.microprofile.health.Readiness; - -// import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; - -// /** -// * Health checks for the Shopping Cart service. -// */ -// @ApplicationScoped -// public class ShoppingCartHealthCheck { - -// @Inject -// private ShoppingCartRepository cartRepository; - -// /** -// * Liveness check for the Shopping Cart service. -// * This check ensures that the application is running. -// * -// * @return A HealthCheckResponse indicating whether the service is alive -// */ -// @Liveness -// public HealthCheck shoppingCartLivenessCheck() { -// return () -> HealthCheckResponse.named("shopping-cart-service-liveness") -// .up() -// .withData("message", "Shopping Cart Service is alive") -// .build(); -// } - -// /** -// * Readiness check for the Shopping Cart service. -// * This check ensures that the application is ready to serve requests. -// * In a real application, this would check dependencies like databases. -// * -// * @return A HealthCheckResponse indicating whether the service is ready -// */ -// @Readiness -// public HealthCheck shoppingCartReadinessCheck() { -// boolean isReady = true; - -// try { -// // Simple check to ensure repository is functioning -// cartRepository.findAll(); -// } catch (Exception e) { -// isReady = false; -// } - -// return () -> { -// if (isReady) { -// return HealthCheckResponse.named("shopping-cart-service-readiness") -// .up() -// .withData("message", "Shopping Cart Service is ready") -// .build(); -// } else { -// return HealthCheckResponse.named("shopping-cart-service-readiness") -// .down() -// .withData("message", "Shopping Cart Service is not ready") -// .build(); -// } -// }; -// } -// } diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java deleted file mode 100644 index 90b3c65a..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java +++ /dev/null @@ -1,199 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.repository; - -import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; -import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Simple in-memory repository for ShoppingCart objects. - * This class provides operations for shopping cart management. - */ -@ApplicationScoped -public class ShoppingCartRepository { - - private final Map carts = new ConcurrentHashMap<>(); - private final Map> cartItems = new ConcurrentHashMap<>(); - private long nextCartId = 1; - private long nextItemId = 1; - - /** - * Finds a shopping cart by user ID. - * - * @param userId The user ID - * @return An Optional containing the shopping cart if found, or empty if not found - */ - public Optional findByUserId(Long userId) { - return carts.values().stream() - .filter(cart -> cart.getUserId().equals(userId)) - .findFirst(); - } - - /** - * Finds a shopping cart by cart ID. - * - * @param cartId The cart ID - * @return An Optional containing the shopping cart if found, or empty if not found - */ - public Optional findById(Long cartId) { - return Optional.ofNullable(carts.get(cartId)); - } - - /** - * Creates a new shopping cart for a user. - * - * @param userId The user ID - * @return The created shopping cart - */ - public ShoppingCart createCart(Long userId) { - ShoppingCart cart = ShoppingCart.builder() - .cartId(nextCartId++) - .userId(userId) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - carts.put(cart.getCartId(), cart); - cartItems.put(cart.getCartId(), new HashMap<>()); - - return cart; - } - - /** - * Adds an item to a shopping cart. - * If the product already exists in the cart, the quantity is increased. - * - * @param cartId The cart ID - * @param item The item to add - * @return The updated cart item - */ - public CartItem addItem(Long cartId, CartItem item) { - Map items = cartItems.get(cartId); - if (items == null) { - throw new IllegalArgumentException("Cart not found: " + cartId); - } - - // Check if the product already exists in the cart - Optional existingItem = items.values().stream() - .filter(i -> i.getProductId().equals(item.getProductId())) - .findFirst(); - - if (existingItem.isPresent()) { - // Update existing item quantity - CartItem updatedItem = existingItem.get(); - updatedItem.setQuantity(updatedItem.getQuantity() + item.getQuantity()); - items.put(updatedItem.getItemId(), updatedItem); - updateCartItems(cartId); - return updatedItem; - } else { - // Add new item - if (item.getItemId() == null) { - item.setItemId(nextItemId++); - } - items.put(item.getItemId(), item); - updateCartItems(cartId); - return item; - } - } - - /** - * Updates an item in a shopping cart. - * - * @param cartId The cart ID - * @param itemId The item ID - * @param item The updated item - * @return The updated cart item - */ - public CartItem updateItem(Long cartId, Long itemId, CartItem item) { - Map items = cartItems.get(cartId); - if (items == null || !items.containsKey(itemId)) { - throw new IllegalArgumentException("Item not found in cart"); - } - - item.setItemId(itemId); - items.put(itemId, item); - updateCartItems(cartId); - - return item; - } - - /** - * Removes an item from a shopping cart. - * - * @param cartId The cart ID - * @param itemId The item ID - * @return true if the item was removed, false otherwise - */ - public boolean removeItem(Long cartId, Long itemId) { - Map items = cartItems.get(cartId); - if (items == null) { - return false; - } - - boolean removed = items.remove(itemId) != null; - if (removed) { - updateCartItems(cartId); - } - - return removed; - } - - /** - * Clears all items from a shopping cart. - * - * @param cartId The cart ID - * @return true if the cart was cleared, false if the cart wasn't found - */ - public boolean clearCart(Long cartId) { - Map items = cartItems.get(cartId); - if (items == null) { - return false; - } - - items.clear(); - updateCartItems(cartId); - - return true; - } - - /** - * Deletes a shopping cart. - * - * @param cartId The cart ID - * @return true if the cart was deleted, false if not found - */ - public boolean deleteCart(Long cartId) { - cartItems.remove(cartId); - return carts.remove(cartId) != null; - } - - /** - * Gets all shopping carts. - * - * @return A list of all shopping carts - */ - public List findAll() { - return new ArrayList<>(carts.values()); - } - - /** - * Updates the items list in a shopping cart and updates the timestamp. - * - * @param cartId The cart ID - */ - private void updateCartItems(Long cartId) { - ShoppingCart cart = carts.get(cartId); - if (cart != null) { - cart.setItems(new ArrayList<>(cartItems.get(cartId).values())); - cart.setUpdatedAt(LocalDateTime.now()); - } - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java deleted file mode 100644 index ec40e55b..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java +++ /dev/null @@ -1,240 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.resource; - -import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; -import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; -import io.microprofile.tutorial.store.shoppingcart.service.ShoppingCartService; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; - -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; -import org.eclipse.microprofile.openapi.annotations.media.Content; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; - -import java.net.URI; -import java.util.List; - -/** - * REST resource for shopping cart operations. - */ -@Path("/carts") -@RequestScoped -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Shopping Cart Resource", description = "Shopping cart management operations") -public class ShoppingCartResource { - - @Inject - private ShoppingCartService cartService; - - @Context - private UriInfo uriInfo; - - @GET - @Operation(summary = "Get all shopping carts", description = "Returns a list of all shopping carts") - @APIResponse( - responseCode = "200", - description = "List of shopping carts", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(type = SchemaType.ARRAY, implementation = ShoppingCart.class) - ) - ) - public List getAllCarts() { - return cartService.getAllCarts(); - } - - @GET - @Path("/{id}") - @Operation(summary = "Get cart by ID", description = "Returns a specific shopping cart by ID") - @APIResponse( - responseCode = "200", - description = "Shopping cart", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = ShoppingCart.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Cart not found" - ) - public ShoppingCart getCartById( - @Parameter(description = "ID of the cart", required = true) - @PathParam("id") Long cartId) { - return cartService.getCartById(cartId); - } - - @GET - @Path("/user/{userId}") - @Operation(summary = "Get cart by user ID", description = "Returns a user's shopping cart") - @APIResponse( - responseCode = "200", - description = "Shopping cart", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = ShoppingCart.class) - ) - ) - @APIResponse( - responseCode = "404", - description = "Cart not found for user" - ) - public Response getCartByUserId( - @Parameter(description = "ID of the user", required = true) - @PathParam("userId") Long userId) { - try { - ShoppingCart cart = cartService.getCartByUserId(userId); - return Response.ok(cart).build(); - } catch (WebApplicationException e) { - if (e.getResponse().getStatus() == Response.Status.NOT_FOUND.getStatusCode()) { - // Create a new cart for the user - ShoppingCart newCart = cartService.getOrCreateCart(userId); - return Response.ok(newCart).build(); - } - throw e; - } - } - - @POST - @Path("/user/{userId}") - @Operation(summary = "Create cart for user", description = "Creates a new shopping cart for a user") - @APIResponse( - responseCode = "201", - description = "Cart created", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = ShoppingCart.class) - ) - ) - public Response createCartForUser( - @Parameter(description = "ID of the user", required = true) - @PathParam("userId") Long userId) { - ShoppingCart cart = cartService.getOrCreateCart(userId); - URI location = uriInfo.getAbsolutePathBuilder().path(cart.getCartId().toString()).build(); - return Response.created(location).entity(cart).build(); - } - - @POST - @Path("/{cartId}/items") - @Operation(summary = "Add item to cart", description = "Adds an item to a shopping cart") - @APIResponse( - responseCode = "200", - description = "Item added", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = CartItem.class) - ) - ) - @APIResponse( - responseCode = "400", - description = "Invalid input or insufficient inventory" - ) - @APIResponse( - responseCode = "404", - description = "Cart not found" - ) - public CartItem addItemToCart( - @Parameter(description = "ID of the cart", required = true) - @PathParam("cartId") Long cartId, - @Parameter(description = "Item to add", required = true) - @NotNull @Valid CartItem item) { - return cartService.addItemToCart(cartId, item); - } - - @PUT - @Path("/{cartId}/items/{itemId}") - @Operation(summary = "Update cart item", description = "Updates an item in a shopping cart") - @APIResponse( - responseCode = "200", - description = "Item updated", - content = @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = CartItem.class) - ) - ) - @APIResponse( - responseCode = "400", - description = "Invalid input or insufficient inventory" - ) - @APIResponse( - responseCode = "404", - description = "Cart or item not found" - ) - public CartItem updateCartItem( - @Parameter(description = "ID of the cart", required = true) - @PathParam("cartId") Long cartId, - @Parameter(description = "ID of the item", required = true) - @PathParam("itemId") Long itemId, - @Parameter(description = "Updated item", required = true) - @NotNull @Valid CartItem item) { - return cartService.updateCartItem(cartId, itemId, item); - } - - @DELETE - @Path("/{cartId}/items/{itemId}") - @Operation(summary = "Remove item from cart", description = "Removes an item from a shopping cart") - @APIResponse( - responseCode = "204", - description = "Item removed" - ) - @APIResponse( - responseCode = "404", - description = "Cart or item not found" - ) - public Response removeItemFromCart( - @Parameter(description = "ID of the cart", required = true) - @PathParam("cartId") Long cartId, - @Parameter(description = "ID of the item", required = true) - @PathParam("itemId") Long itemId) { - cartService.removeItemFromCart(cartId, itemId); - return Response.noContent().build(); - } - - @DELETE - @Path("/{cartId}/items") - @Operation(summary = "Clear cart", description = "Removes all items from a shopping cart") - @APIResponse( - responseCode = "204", - description = "Cart cleared" - ) - @APIResponse( - responseCode = "404", - description = "Cart not found" - ) - public Response clearCart( - @Parameter(description = "ID of the cart", required = true) - @PathParam("cartId") Long cartId) { - cartService.clearCart(cartId); - return Response.noContent().build(); - } - - @DELETE - @Path("/{cartId}") - @Operation(summary = "Delete cart", description = "Deletes a shopping cart") - @APIResponse( - responseCode = "204", - description = "Cart deleted" - ) - @APIResponse( - responseCode = "404", - description = "Cart not found" - ) - public Response deleteCart( - @Parameter(description = "ID of the cart", required = true) - @PathParam("cartId") Long cartId) { - cartService.deleteCart(cartId); - return Response.noContent().build(); - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java b/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java deleted file mode 100644 index bc39375f..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java +++ /dev/null @@ -1,223 +0,0 @@ -package io.microprofile.tutorial.store.shoppingcart.service; - -import io.microprofile.tutorial.store.shoppingcart.client.CatalogClient; -import io.microprofile.tutorial.store.shoppingcart.client.InventoryClient; -import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; -import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; -import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response; - -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * Service class for Shopping Cart management operations. - */ -@ApplicationScoped -public class ShoppingCartService { - - private static final Logger LOGGER = Logger.getLogger(ShoppingCartService.class.getName()); - - @Inject - private ShoppingCartRepository cartRepository; - - @Inject - private InventoryClient inventoryClient; - - @Inject - private CatalogClient catalogClient; - - /** - * Gets a shopping cart for a user, creating one if it doesn't exist. - * - * @param userId The user ID - * @return The user's shopping cart - */ - public ShoppingCart getOrCreateCart(Long userId) { - Optional existingCart = cartRepository.findByUserId(userId); - - return existingCart.orElseGet(() -> { - LOGGER.info("Creating new cart for user: " + userId); - return cartRepository.createCart(userId); - }); - } - - /** - * Gets a shopping cart by ID. - * - * @param cartId The cart ID - * @return The shopping cart - * @throws WebApplicationException if the cart is not found - */ - public ShoppingCart getCartById(Long cartId) { - return cartRepository.findById(cartId) - .orElseThrow(() -> new WebApplicationException("Cart not found", Response.Status.NOT_FOUND)); - } - - /** - * Gets a user's shopping cart. - * - * @param userId The user ID - * @return The user's shopping cart - * @throws WebApplicationException if the cart is not found - */ - public ShoppingCart getCartByUserId(Long userId) { - return cartRepository.findByUserId(userId) - .orElseThrow(() -> new WebApplicationException("Cart not found for user", Response.Status.NOT_FOUND)); - } - - /** - * Gets all shopping carts. - * - * @return A list of all shopping carts - */ - public List getAllCarts() { - return cartRepository.findAll(); - } - - /** - * Adds an item to a shopping cart. - * - * @param cartId The cart ID - * @param item The item to add - * @return The updated cart item - * @throws WebApplicationException if the cart is not found or inventory is insufficient - */ - public CartItem addItemToCart(Long cartId, CartItem item) { - // Verify the cart exists - getCartById(cartId); - - // Check inventory availability - boolean isAvailable = inventoryClient.checkProductAvailability(item.getProductId(), item.getQuantity()); - if (!isAvailable) { - throw new WebApplicationException("Insufficient inventory for product: " + item.getProductId(), - Response.Status.BAD_REQUEST); - } - - // Enrich item with product details if needed - if (item.getProductName() == null || item.getPrice() == 0) { - CatalogClient.ProductInfo productInfo = catalogClient.getProductInfo(item.getProductId()); - item.setProductName(productInfo.getName()); - item.setPrice(productInfo.getPrice()); - } - - LOGGER.info(String.format("Adding item to cart %d: %s, quantity %d", - cartId, item.getProductName(), item.getQuantity())); - - return cartRepository.addItem(cartId, item); - } - - /** - * Updates an item in a shopping cart. - * - * @param cartId The cart ID - * @param itemId The item ID - * @param item The updated item - * @return The updated cart item - * @throws WebApplicationException if the cart or item is not found or inventory is insufficient - */ - public CartItem updateCartItem(Long cartId, Long itemId, CartItem item) { - // Verify the cart exists - ShoppingCart cart = getCartById(cartId); - - // Verify the item exists - Optional existingItem = cart.getItems().stream() - .filter(i -> i.getItemId().equals(itemId)) - .findFirst(); - - if (!existingItem.isPresent()) { - throw new WebApplicationException("Item not found in cart", Response.Status.NOT_FOUND); - } - - // Check inventory availability if quantity is increasing - CartItem currentItem = existingItem.get(); - if (item.getQuantity() > currentItem.getQuantity()) { - int additionalQuantity = item.getQuantity() - currentItem.getQuantity(); - boolean isAvailable = inventoryClient.checkProductAvailability( - currentItem.getProductId(), additionalQuantity); - - if (!isAvailable) { - throw new WebApplicationException("Insufficient inventory for product: " + currentItem.getProductId(), - Response.Status.BAD_REQUEST); - } - } - - // Preserve product information - item.setProductId(currentItem.getProductId()); - - // If no product name is provided, use the existing one - if (item.getProductName() == null) { - item.setProductName(currentItem.getProductName()); - } - - // If no price is provided, use the existing one - if (item.getPrice() == 0) { - item.setPrice(currentItem.getPrice()); - } - - LOGGER.info(String.format("Updating item %d in cart %d: new quantity %d", - itemId, cartId, item.getQuantity())); - - return cartRepository.updateItem(cartId, itemId, item); - } - - /** - * Removes an item from a shopping cart. - * - * @param cartId The cart ID - * @param itemId The item ID - * @throws WebApplicationException if the cart or item is not found - */ - public void removeItemFromCart(Long cartId, Long itemId) { - // Verify the cart exists - getCartById(cartId); - - boolean removed = cartRepository.removeItem(cartId, itemId); - if (!removed) { - throw new WebApplicationException("Item not found in cart", Response.Status.NOT_FOUND); - } - - LOGGER.info(String.format("Removed item %d from cart %d", itemId, cartId)); - } - - /** - * Clears all items from a shopping cart. - * - * @param cartId The cart ID - * @throws WebApplicationException if the cart is not found - */ - public void clearCart(Long cartId) { - // Verify the cart exists - getCartById(cartId); - - boolean cleared = cartRepository.clearCart(cartId); - if (!cleared) { - throw new WebApplicationException("Failed to clear cart", Response.Status.INTERNAL_SERVER_ERROR); - } - - LOGGER.info(String.format("Cleared cart %d", cartId)); - } - - /** - * Deletes a shopping cart. - * - * @param cartId The cart ID - * @throws WebApplicationException if the cart is not found - */ - public void deleteCart(Long cartId) { - // Verify the cart exists - getCartById(cartId); - - boolean deleted = cartRepository.deleteCart(cartId); - if (!deleted) { - throw new WebApplicationException("Failed to delete cart", Response.Status.INTERNAL_SERVER_ERROR); - } - - LOGGER.info(String.format("Deleted cart %d", cartId)); - } -} diff --git a/code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index 9990f3d1..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,16 +0,0 @@ -# Shopping Cart Service Configuration -mp.openapi.scan=true - -# Service URLs -inventory.service.url=https://scaling-pancake-77vj4pwq7fpjqx-7050.app.github.dev/ -catalog.service.url=https://scaling-pancake-77vj4pwq7fpjqx-5050.app.github.dev/ -user.service.url=https://scaling-pancake-77vj4pwq7fpjqx-6050.app.github.dev/ - -# Fault Tolerance Configuration -# circuitBreaker.delay=10000 -# circuitBreaker.requestVolumeThreshold=4 -# circuitBreaker.failureRatio=0.5 -# retry.maxRetries=3 -# retry.delay=1000 -# retry.jitter=200 -# timeout.value=5000 diff --git a/code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml b/code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 383982d7..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Shopping Cart Service - - - index.html - index.jsp - - diff --git a/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html b/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html deleted file mode 100644 index d2d25193..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - Shopping Cart Service - MicroProfile E-Commerce - - - -
-

Shopping Cart Service

-

Part of the MicroProfile E-Commerce Application

-
- -
-
-

About this Service

-

The Shopping Cart Service manages user shopping carts in the e-commerce system.

-

It provides endpoints for creating carts, adding/removing items, and managing cart contents.

-

This service integrates with the Inventory service to check product availability and the Catalog service to get product details.

-
- -
-

API Endpoints

-
    -
  • GET /api/carts - Get all shopping carts
  • -
  • GET /api/carts/{id} - Get cart by ID
  • -
  • GET /api/carts/user/{userId} - Get cart by user ID
  • -
  • POST /api/carts/user/{userId} - Create cart for user
  • -
  • POST /api/carts/{cartId}/items - Add item to cart
  • -
  • PUT /api/carts/{cartId}/items/{itemId} - Update cart item
  • -
  • DELETE /api/carts/{cartId}/items/{itemId} - Remove item from cart
  • -
  • DELETE /api/carts/{cartId}/items - Clear cart
  • -
  • DELETE /api/carts/{cartId} - Delete cart
  • -
-
- - -
- -
-

MicroProfile E-Commerce Demo Application | Shopping Cart Service

-

Powered by Open Liberty & MicroProfile

-
- - diff --git a/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp b/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp deleted file mode 100644 index 1fcd4190..00000000 --- a/code/chapter11/chapter11/shoppingcart/src/main/webapp/index.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> - - - - - - Redirecting... - - -

Redirecting to the Shopping Cart Service homepage...

- - From 01aae140f1629227eeb0d5fe5211d2e7ce63302b Mon Sep 17 00:00:00 2001 From: Tarun Telang Date: Fri, 24 Apr 2026 18:25:17 +0000 Subject: [PATCH 7/7] Updating code for chapter11 --- code/chapter11/README.adoc | 1111 +++++++++++++++++ code/chapter11/catalog/README.adoc | 622 +++++++++ code/chapter11/catalog/pom.xml | 85 ++ .../store/product/ProductRestApplication.java | 9 + .../store/product/entity/Product.java | 16 + .../product/repository/ProductRepository.java | 138 ++ .../product/resource/ProductResource.java | 182 +++ .../store/product/service/ProductService.java | 97 ++ .../META-INF/microprofile-config.properties | 5 + .../catalog/src/main/webapp/WEB-INF/web.xml | 13 + .../catalog/src/main/webapp/index.html | 281 +++++ code/chapter11/docker-compose.yml | 85 ++ code/chapter11/inventory/README.adoc | 387 ++++++ code/chapter11/inventory/pom.xml | 163 +++ .../store/inventory/InventoryApplication.java | 33 + .../client/ProductServiceClient.java | 23 + .../dto/InventoryWithProductInfo.java | 165 +++ .../tutorial/store/inventory/dto/Product.java | 69 + .../store/inventory/entity/Inventory.java | 51 + .../inventory/exception/ErrorResponse.java | 103 ++ .../exception/InventoryConflictException.java | 41 + .../exception/InventoryExceptionMapper.java | 46 + .../exception/InventoryNotFoundException.java | 40 + .../store/inventory/package-info.java | 13 + .../repository/InventoryRepository.java | 168 +++ .../inventory/resource/InventoryResource.java | 266 ++++ .../inventory/service/InventoryService.java | 493 ++++++++ .../META-INF/microprofile-config.properties | 5 + .../src/main/webapp/WEB-INF/beans.xml | 7 + .../inventory/src/main/webapp/WEB-INF/web.xml | 10 + .../inventory/src/main/webapp/index.html | 350 ++++++ .../InventoryServiceIntegrationTest.java | 154 +++ .../service/InventoryServiceTest.java | 302 +++++ code/chapter11/order/Dockerfile | 19 + code/chapter11/order/README.md | 147 +++ code/chapter11/order/pom.xml | 114 ++ code/chapter11/order/restart-server.sh | 35 + code/chapter11/order/run-docker.sh | 10 + code/chapter11/order/run.sh | 12 + .../store/order/OrderApplication.java | 34 + .../tutorial/store/order/entity/Order.java | 45 + .../store/order/entity/OrderItem.java | 38 + .../store/order/entity/OrderStatus.java | 14 + .../tutorial/store/order/package-info.java | 14 + .../order/repository/OrderItemRepository.java | 124 ++ .../order/repository/OrderRepository.java | 109 ++ .../order/resource/OrderItemResource.java | 149 +++ .../store/order/resource/OrderResource.java | 208 +++ .../store/order/service/OrderService.java | 360 ++++++ .../order/src/main/webapp/WEB-INF/web.xml | 10 + .../order/src/main/webapp/index.html | 144 +++ .../src/main/webapp/order-status-codes.html | 75 ++ code/chapter11/payment/Dockerfile | 20 + code/chapter11/payment/README.adoc | 266 ++++ code/chapter11/payment/pom.xml | 95 ++ code/chapter11/payment/run-docker.sh | 23 + code/chapter11/payment/run.sh | 19 + .../tutorial/PaymentRestApplication.java | 9 + .../store/payment/client/ProductClient.java | 67 + .../client/ProductClientWithFilters.java | 89 ++ .../client/ProductJakartaRestClient.java | 55 + .../ProductJakartaRestClientSimple.java | 53 + ...ProductServiceResponseExceptionMapper.java | 140 +++ .../store/payment/config/PaymentConfig.java | 63 + .../config/PaymentServiceConfigSource.java | 60 + .../payment/demo/ProductClientRunner.java | 72 ++ .../store/payment/dto/product/Product.java | 10 + .../store/payment/entity/PaymentDetails.java | 18 + .../exception/ProductNotFoundException.java | 31 + .../ServiceUnavailableException.java | 32 + .../payment/filter/BearerTokenFilter.java | 72 ++ .../payment/filter/CorrelationIdFilter.java | 77 ++ .../payment/filter/RequestLoggingFilter.java | 100 ++ .../payment/filter/ResponseLoggingFilter.java | 136 ++ .../resource/PaymentConfigResource.java | 98 ++ .../resource/PaymentProductResource.java | 186 +++ .../resource/ProductCatalogResource.java | 853 +++++++++++++ .../FilteredProductCatalogService.java | 124 ++ .../store/payment/service/PaymentService.java | 46 + .../service/ProductCatalogService.java | 154 +++ .../service/ProductClientBuilderService.java | 221 ++++ .../service/ProductIntegrationService.java | 148 +++ .../store/payment/service/payment.http | 9 + .../META-INF/microprofile-config.properties | 35 + ...lipse.microprofile.config.spi.ConfigSource | 1 + .../payment/src/main/webapp/WEB-INF/beans.xml | 16 + .../payment/src/main/webapp/WEB-INF/web.xml | 12 + .../payment/src/main/webapp/index.html | 140 +++ .../payment/src/main/webapp/index.jsp | 12 + code/chapter11/run-all-services.sh | 36 + code/chapter11/shipment/Dockerfile | 27 + code/chapter11/shipment/README.md | 87 ++ code/chapter11/shipment/pom.xml | 114 ++ code/chapter11/shipment/run-docker.sh | 11 + code/chapter11/shipment/run.sh | 12 + .../store/shipment/ShipmentApplication.java | 35 + .../store/shipment/client/OrderClient.java | 193 +++ .../store/shipment/entity/Shipment.java | 45 + .../store/shipment/entity/ShipmentStatus.java | 16 + .../store/shipment/filter/CorsFilter.java | 43 + .../shipment/health/ShipmentHealthCheck.java | 67 + .../repository/ShipmentRepository.java | 148 +++ .../shipment/resource/ShipmentResource.java | 397 ++++++ .../shipment/service/ShipmentService.java | 305 +++++ .../META-INF/microprofile-config.properties | 32 + .../shipment/src/main/webapp/WEB-INF/web.xml | 13 + .../shipment/src/main/webapp/index.html | 150 +++ code/chapter11/shoppingcart/Dockerfile | 20 + code/chapter11/shoppingcart/README.md | 87 ++ code/chapter11/shoppingcart/pom.xml | 114 ++ code/chapter11/shoppingcart/run-docker.sh | 23 + code/chapter11/shoppingcart/run.sh | 19 + .../shoppingcart/ShoppingCartApplication.java | 12 + .../shoppingcart/client/CatalogClient.java | 184 +++ .../shoppingcart/client/InventoryClient.java | 96 ++ .../store/shoppingcart/entity/CartItem.java | 32 + .../shoppingcart/entity/ShoppingCart.java | 57 + .../health/ShoppingCartHealthCheck.java | 68 + .../repository/ShoppingCartRepository.java | 199 +++ .../resource/ShoppingCartResource.java | 240 ++++ .../service/ShoppingCartService.java | 223 ++++ .../META-INF/microprofile-config.properties | 16 + .../src/main/webapp/WEB-INF/web.xml | 12 + .../shoppingcart/src/main/webapp/index.html | 128 ++ .../shoppingcart/src/main/webapp/index.jsp | 12 + code/chapter11/user/Dockerfile | 19 + code/chapter11/user/README.adoc | 280 +++++ code/chapter11/user/pom.xml | 115 ++ .../tutorial/store/user/UserApplication.java | 12 + .../tutorial/store/user/entity/User.java | 75 ++ .../store/user/entity/package-info.java | 6 + .../tutorial/store/user/package-info.java | 6 + .../store/user/repository/UserRepository.java | 135 ++ .../store/user/repository/package-info.java | 6 + .../store/user/resource/UserResource.java | 132 ++ .../store/user/resource/package-info.java | 0 .../store/user/service/UserService.java | 130 ++ .../store/user/service/package-info.java | 0 .../chapter11/user/src/main/webapp/index.html | 107 ++ 139 files changed, 15052 insertions(+) create mode 100644 code/chapter11/README.adoc create mode 100644 code/chapter11/catalog/README.adoc create mode 100644 code/chapter11/catalog/pom.xml create mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java create mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java create mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java create mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java create mode 100644 code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java create mode 100644 code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties create mode 100644 code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/catalog/src/main/webapp/index.html create mode 100644 code/chapter11/docker-compose.yml create mode 100644 code/chapter11/inventory/README.adoc create mode 100644 code/chapter11/inventory/pom.xml create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java create mode 100644 code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java create mode 100644 code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties create mode 100644 code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml create mode 100644 code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/inventory/src/main/webapp/index.html create mode 100644 code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java create mode 100644 code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java create mode 100644 code/chapter11/order/Dockerfile create mode 100644 code/chapter11/order/README.md create mode 100644 code/chapter11/order/pom.xml create mode 100644 code/chapter11/order/restart-server.sh create mode 100644 code/chapter11/order/run-docker.sh create mode 100644 code/chapter11/order/run.sh create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java create mode 100644 code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java create mode 100644 code/chapter11/order/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/order/src/main/webapp/index.html create mode 100644 code/chapter11/order/src/main/webapp/order-status-codes.html create mode 100644 code/chapter11/payment/Dockerfile create mode 100644 code/chapter11/payment/README.adoc create mode 100644 code/chapter11/payment/pom.xml create mode 100644 code/chapter11/payment/run-docker.sh create mode 100644 code/chapter11/payment/run.sh create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClientSimple.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/FilteredProductCatalogService.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java create mode 100644 code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http create mode 100644 code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties create mode 100644 code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource create mode 100644 code/chapter11/payment/src/main/webapp/WEB-INF/beans.xml create mode 100644 code/chapter11/payment/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/payment/src/main/webapp/index.html create mode 100644 code/chapter11/payment/src/main/webapp/index.jsp create mode 100644 code/chapter11/run-all-services.sh create mode 100644 code/chapter11/shipment/Dockerfile create mode 100644 code/chapter11/shipment/README.md create mode 100644 code/chapter11/shipment/pom.xml create mode 100644 code/chapter11/shipment/run-docker.sh create mode 100644 code/chapter11/shipment/run.sh create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java create mode 100644 code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java create mode 100644 code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties create mode 100644 code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/shipment/src/main/webapp/index.html create mode 100644 code/chapter11/shoppingcart/Dockerfile create mode 100644 code/chapter11/shoppingcart/README.md create mode 100644 code/chapter11/shoppingcart/pom.xml create mode 100644 code/chapter11/shoppingcart/run-docker.sh create mode 100644 code/chapter11/shoppingcart/run.sh create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java create mode 100644 code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java create mode 100644 code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties create mode 100644 code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml create mode 100644 code/chapter11/shoppingcart/src/main/webapp/index.html create mode 100644 code/chapter11/shoppingcart/src/main/webapp/index.jsp create mode 100644 code/chapter11/user/Dockerfile create mode 100644 code/chapter11/user/README.adoc create mode 100644 code/chapter11/user/pom.xml create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java create mode 100644 code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java create mode 100644 code/chapter11/user/src/main/webapp/index.html diff --git a/code/chapter11/README.adoc b/code/chapter11/README.adoc new file mode 100644 index 00000000..593f1c90 --- /dev/null +++ b/code/chapter11/README.adoc @@ -0,0 +1,1111 @@ += MicroProfile E-Commerce Store +:toc: left +:icons: font +:source-highlighter: highlightjs +:imagesdir: images +:experimental: + +== Overview + +This project demonstrates a microservices-based e-commerce application built with Jakarta EE and MicroProfile, running on Open Liberty runtime. +The application is composed of multiple independent services that work together to provide a complete e-commerce solution. + +== Services + +The application is split into the following microservices: + +[cols="1,4", options="header"] +|=== +|Service |Description + +|User Service +|Manages user accounts, authentication, and profile information + +|Inventory Service +|Tracks product inventory and stock levels + +|Order Service +|Manages customer orders, order items, and order status + +|Catalog Service +|Provides product information, categories, and search capabilities + +|Shopping Cart Service +|Manages user shopping cart items and temporary product storage + +|Shipment Service +|Handles shipping orders, tracking, and delivery status updates + +|Payment Service +|Processes payments and manages payment methods and transactions. Demonstrates MicroProfile Rest Client 4.0 features including CDI injection, ResponseExceptionMapper, custom filters, and programmatic client creation. +|=== + +== Technology Stack + +* *Jakarta EE 10.0*: For enterprise Java standardization +* *MicroProfile 7.x*: For cloud-native APIs +* *MicroProfile Rest Client 4.0*: Type-safe REST service consumption +* *Open Liberty*: Lightweight, flexible runtime for Java microservices +* *Maven*: For project management and builds + +== Quick Start + +=== Prerequisites + +* JDK 21 or later +* Maven 3.9 or later +* Docker (optional for containerized deployment) + +=== Running the Application + +1. Clone the repository: ++ +[source,bash] +---- +git clone https://github.com/microprofile/microprofile-tutorial.git +cd code/chapter11 +---- + +2. Start each microservice individually: + +==== User Service +[source,bash] +---- +cd user +mvn liberty:run +---- +The service will be available at http://localhost:6050/user + +==== Inventory Service +[source,bash] +---- +cd inventory +mvn liberty:run +---- +The service will be available at http://localhost:7050/inventory + +==== Order Service +[source,bash] +---- +cd order +mvn liberty:run +---- +The service will be available at http://localhost:8050/order + +==== Catalog Service +[source,bash] +---- +cd catalog +mvn liberty:run +---- +The service will be available at http://localhost:9050/catalog + +=== Building the Application + +To build all services: + +[source,bash] +---- +mvn clean package +---- + +=== Docker Deployment + +You can also run all services together using Docker Compose: + +[source,bash] +---- +# Make the script executable (if needed) +chmod +x run-all-services.sh + +# Run the script to build and start all services +./run-all-services.sh +---- + +Or manually: + +[source,bash] +---- +# Build all projects first +cd user && mvn clean package && cd .. +cd inventory && mvn clean package && cd .. +cd order && mvn clean package && cd .. +cd catalog && mvn clean package && cd .. + +# Start all services +docker-compose up -d +---- + +This will start all services in Docker containers with the following endpoints: + +* User Service: http://localhost:6050/user +* Inventory Service: http://localhost:7050/inventory +* Order Service: http://localhost:8050/order +* Catalog Service: http://localhost:9050/catalog + +== API Documentation + +Each microservice provides its own OpenAPI documentation, available at: + +* User Service: http://localhost:6050/user/openapi +* Inventory Service: http://localhost:7050/inventory/openapi +* Order Service: http://localhost:8050/order/openapi +* Catalog Service: http://localhost:9050/catalog/openapi + +== MicroProfile Rest Client 4.0 Features + +This application demonstrates the latest MicroProfile Rest Client 4.0 features: + +=== New `baseUri(String)` Convenience Method + +The Inventory Service showcases the new `baseUri(String)` method that eliminates the need for `URI.create()`: + +[source,java] +---- +// MicroProfile Rest Client 4.0 - Simplified approach +ProductServiceClient client = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") // Direct String parameter + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(ProductServiceClient.class); +---- + +This replaces the older pattern: + +[source,java] +---- +// Old approach (Rest Client 3.x) +URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); +ProductServiceClient client = RestClientBuilder.newBuilder() + .baseUri(catalogServiceUri) // Required URI object + .build(ProductServiceClient.class); +---- + +=== CDI Integration with `@RestClient` + +The `@RestClient` qualifier is mandatory for CDI injection: + +[source,java] +---- +@Inject +@RestClient // Required qualifier +private ProductServiceClient productClient; +---- + +=== AutoCloseable Support + +REST client interfaces extend `AutoCloseable` for proper resource management: + +[source,java] +---- +@RegisterRestClient(configKey = "product-service") +@Path("/products") +public interface ProductServiceClient extends AutoCloseable { + // Client methods +} +---- + +See the `InventoryService` class for complete working examples of these features. + +== Testing the Services + +=== User Service + +[source,bash] +---- +# Get all users +curl -X GET http://localhost:6050/user/api/users + +# Create a new user +curl -X POST http://localhost:6050/user/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Jane Doe", + "email": "jane@example.com", + "passwordHash": "password123", + "address": "123 Main St", + "phoneNumber": "555-123-4567" + }' + +# Get a user by ID +curl -X GET http://localhost:6050/user/api/users/1 + +# Update a user +curl -X PUT http://localhost:6050/user/api/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Jane Smith", + "email": "jane@example.com", + "passwordHash": "password123", + "address": "456 Oak Ave", + "phoneNumber": "555-123-4567" + }' + +# Delete a user +curl -X DELETE http://localhost:6050/user/api/users/1 +---- + +=== Inventory Service + +[source,bash] +---- +# Get all inventory items +curl -X GET http://localhost:7050/inventory/api/inventories + +# Create a new inventory item +curl -X POST http://localhost:7050/inventory/api/inventories \ + -H "Content-Type: application/json" \ + -d '{ + "productId": 101, + "quantity": 25 + }' + +# Get inventory by ID +curl -X GET http://localhost:7050/inventory/api/inventories/1 + +# Get inventory by product ID +curl -X GET http://localhost:7050/inventory/api/inventories/product/101 + +# Update inventory +curl -X PUT http://localhost:7050/inventory/api/inventories/1 \ + -H "Content-Type: application/json" \ + -d '{ + "productId": 101, + "quantity": 50 + }' + +# Update product quantity +curl -X PATCH http://localhost:7050/inventory/api/inventories/product/101/quantity/75 + +# Delete inventory +curl -X DELETE http://localhost:7050/inventory/api/inventories/1 +---- + +=== Order Service + +[source,bash] +---- +# Get all orders +curl -X GET http://localhost:8050/order/api/orders + +# Create a new order +curl -X POST http://localhost:8050/order/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "totalPrice": 149.98, + "status": "CREATED", + "orderItems": [ + { + "productId": 101, + "quantity": 2, + "priceAtOrder": 49.99 + }, + { + "productId": 102, + "quantity": 1, + "priceAtOrder": 50.00 + } + ] + }' + +# Get order by ID +curl -X GET http://localhost:8050/order/api/orders/1 + +# Update order status +curl -X PATCH http://localhost:8050/order/api/orders/1/status/PAID + +# Get items for an order +curl -X GET http://localhost:8050/order/api/orders/1/items + +# Delete order +curl -X DELETE http://localhost:8050/order/api/orders/1 +---- + +=== Catalog Service + +[source,bash] +---- +# Get all products +curl -X GET http://localhost:9050/catalog/api/products + +# Get a product by ID +curl -X GET http://localhost:9050/catalog/api/products/1 + +# Search products +curl -X GET "http://localhost:9050/catalog/api/products/search?keyword=laptop" +---- + +== Verifying MicroProfile Rest Client Examples + +The Payment Service demonstrates comprehensive MicroProfile Rest Client 4.0 features. Follow these steps to verify each example. + +=== Prerequisites + +Before testing the examples, ensure both services are running: + +[source,bash] +---- +# Terminal 1 - Start the Catalog Service (provides product data) +cd catalog +mvn liberty:run + +# Terminal 2 - Start the Payment Service (consumes catalog service) +cd payment +mvn liberty:run +---- + +The services will be available at: + +* Catalog Service: http://localhost:5050/catalog +* Payment Service: http://localhost:9050/payment + +=== Example 1: Basic REST Client with @RegisterRestClient + +This example demonstrates defining a MicroProfile Rest Client interface with `@RegisterRestClient`. + +**What to Verify:** + +* `ProductClient` interface is annotated with `@RegisterRestClient(configKey = "catalog-service")` +* The client uses Jakarta REST annotations (`@GET`, `@Path`, `@PathParam`) +* Configuration is externalized in `microprofile-config.properties` + +**Test the Client:** + +[source,bash] +---- +# Get all products via REST client +curl http://localhost:9050/payment/catalog/products + +# Expected Response: JSON array of products +[ + { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + ... +] +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` +* `src/main/resources/META-INF/microprofile-config.properties` + +=== Example 2: MicroProfile Config Integration + +This example shows externalized configuration for REST clients using MicroProfile Config. + +**What to Verify:** + +* Configuration properties follow the pattern: `/mp-rest/` +* Base URL, timeouts, and scope are configured +* Properties can be overridden via environment variables + +**Configuration Properties:** + +[source,properties] +---- +# Base URL +catalog-service/mp-rest/url=http://localhost:5050/catalog/api + +# Timeouts (in milliseconds) +catalog-service/mp-rest/connectTimeout=3000 +catalog-service/mp-rest/readTimeout=5000 + +# CDI Scope +catalog-service/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped + +# Follow redirects +catalog-service/mp-rest/followRedirects=true +---- + +**Test Configuration:** + +[source,bash] +---- +# Verify the client respects timeout settings +# (This will timeout if catalog service is slow) +time curl http://localhost:9050/payment/catalog/products +---- + +**Files to Review:** + +* `src/main/resources/META-INF/microprofile-config.properties` + +=== Example 3: CDI Injection with @Inject and @RestClient + +This example demonstrates dependency injection of REST clients using CDI. + +**What to Verify:** + +* `ProductClient` is injected with `@Inject` and `@RestClient` qualifiers +* The `@RestClient` qualifier is mandatory in MicroProfile Rest Client 4.0 +* Service layer (`ProductCatalogService`) uses the injected client +* `@ApplicationScoped` bean lifecycle management + +**Test CDI Injection:** + +[source,bash] +---- +# Test service that uses injected REST client +curl http://localhost:9050/payment/catalog/products + +# Get specific product (uses getProductById method) +curl http://localhost:9050/payment/catalog/products/1 + +# Expected Response: +{ + "id": 1, + "name": "Laptop", + "price": 999.99 +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java` +* `src/main/webapp/WEB-INF/beans.xml` (CDI configuration) + +=== Example 4: Parameter Annotations (@PathParam, @QueryParam) + +This example shows different parameter types in REST client methods. + +**What to Verify:** + +* `@PathParam` for path variables (e.g., `/products/{id}`) +* `@QueryParam` for query string parameters (e.g., `?price=99.99`) +* Parameters are automatically encoded and passed to remote service + +**Test Path Parameters:** + +[source,bash] +---- +# Test @PathParam - Get product by ID +curl http://localhost:9050/payment/catalog/products/1 + +# Test with different IDs +curl http://localhost:9050/payment/catalog/products/2 +curl http://localhost:9050/payment/catalog/products/3 +---- + +**Test Query Parameters:** + +[source,bash] +---- +# Test @QueryParam - Validate product price +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" + +# Expected Response (if price matches): +{ + "productId": 1, + "expectedPrice": 999.99, + "valid": true, + "message": "Price matches" +} + +# Test with wrong price +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=500.00" + +# Expected Response (if price doesn't match): +{ + "productId": 1, + "expectedPrice": 500.0, + "valid": false, + "message": "Price mismatch detected" +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (method signatures) +* `src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java` (endpoint implementation) + +=== Example 5: ResponseExceptionMapper - Custom Error Handling + +This example demonstrates mapping HTTP error responses to custom exceptions. + +**What to Verify:** + +* `ProductServiceResponseExceptionMapper` implements `ResponseExceptionMapper` +* HTTP 404 responses map to `ProductNotFoundException` (checked exception) +* HTTP 503 responses map to `ServiceUnavailableException` (unchecked exception) +* Error messages are extracted from JSON response bodies +* `@Priority` annotation controls mapper ordering + +**Test Exception Mapping:** + +[source,bash] +---- +# Test successful request (no exception) +curl http://localhost:9050/payment/catalog/products/1/detailed + +# Expected Response: +{ + "productId": 1, + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "note": "Product retrieved successfully - no exceptions thrown" +} + +# Test 404 error (ProductNotFoundException) +curl http://localhost:9050/payment/catalog/products/999999/detailed + +# Expected Response: +{ + "productId": 999999, + "error": "Product not found", + "note": "ProductNotFoundException was caught and handled by the service layer" +} + +# Test product availability (handles 404 gracefully) +curl http://localhost:9050/payment/catalog/products/999999/availability + +# Expected Response: +{ + "productId": 999999, + "available": false, + "message": "Product is not available" +} +---- + +**Verify Exception Types:** + +1. **Checked Exception (ProductNotFoundException)**: + - Must be declared in method signature: `throws ProductNotFoundException` + - Only thrown if method declares it + - Handled gracefully in service layer + +2. **Unchecked Exception (ServiceUnavailableException)**: + - Extends `RuntimeException` + - Always thrown regardless of method signature + - Used for service availability errors + +**Test Service Unavailable Scenario:** + +[source,bash] +---- +# Stop the catalog service to simulate 503 error +# Then try to access products +curl http://localhost:9050/payment/catalog/products/1/detailed + +# Expected Response (if catalog service is down): +{ + "productId": 1, + "error": "Catalog service unavailable", + "statusCode": 503, + "message": "...", + "note": "ServiceUnavailableException (unchecked) was thrown by ResponseExceptionMapper" +} +---- + +**Files to Review:** + +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java` +* `src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java` +* `src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java` +* `src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java` (see `@RegisterProvider`) + +=== Verifying beans.xml Configuration + +The `beans.xml` file is required for CDI bean discovery and REST client registration. + +**What to Verify:** + +* File location: `src/main/webapp/WEB-INF/beans.xml` +* Bean discovery mode: `bean-discovery-mode="all"` +* Version: Jakarta EE 10 (beans 4.0) + +[source,xml] +---- + + + +---- + +**Files to Review:** + +* `src/main/webapp/WEB-INF/beans.xml` + +=== Additional Testing Scenarios + +==== Test Complete Product Validation Flow + +[source,bash] +---- +# 1. Check if product is available +curl http://localhost:9050/payment/catalog/products/1/availability + +# 2. Validate product price before payment +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=999.99" + +# 3. Get product details for payment processing +curl http://localhost:9050/payment/catalog/products/1 +---- + +==== Test Error Scenarios + +[source,bash] +---- +# Invalid product ID (404) +curl http://localhost:9050/payment/catalog/products/999999 + +# Missing query parameter (400) +curl http://localhost:9050/payment/catalog/products/1/validate-price + +# Invalid price value (400) +curl "http://localhost:9050/payment/catalog/products/1/validate-price?price=-10" +---- + +=== Viewing OpenAPI Documentation + +Access the OpenAPI documentation to see all available endpoints: + +[source,bash] +---- +# Payment Service OpenAPI documentation +open http://localhost:9050/openapi/ui/ + +# Or view the OpenAPI JSON spec +curl http://localhost:9050/openapi +---- + +=== Debugging Tips + +**Enable Detailed Logging:** + +Edit `src/main/liberty/config/server.xml` and add: + +[source,xml] +---- + +---- + +**Check Server Logs:** + +[source,bash] +---- +# View real-time logs +tail -f target/liberty/wlp/usr/servers/mpServer/logs/messages.log + +# Search for REST client activity +grep "ProductClient" target/liberty/wlp/usr/servers/mpServer/logs/messages.log +---- + +**Verify Configuration Loading:** + +[source,bash] +---- +# Check if properties are loaded correctly +# Look for log messages about MicroProfile Config +grep "mp-rest" target/liberty/wlp/usr/servers/mpServer/logs/messages.log +---- + +=== Example 6: Custom Filters and Interceptors + +This example demonstrates implementing custom filters for cross-cutting concerns like authentication, logging, and distributed tracing. + +**What to Verify:** + +* `ClientRequestFilter` implementations for request interception +* `ClientResponseFilter` implementations for response logging +* Filter registration using `@RegisterProvider` +* Filter execution order using `@Priority` +* Cross-cutting concerns separated from business logic + +**Filters Implemented:** + +1. **BearerTokenFilter** (Priority 1000 - AUTHENTICATION) + - Adds Authorization header with Bearer token + - Reads token from MicroProfile Config + - Runs first due to authentication priority + +2. **CorrelationIdFilter** (Priority 100) + - Adds X-Correlation-ID for distributed tracing + - Generates or propagates correlation IDs + - Adds unique X-Request-ID for each request + +3. **RequestLoggingFilter** (Priority 300) + - Logs complete outgoing request details + - Masks sensitive headers (Authorization, API keys) + - Runs after authentication and correlation filters + +4. **ResponseLoggingFilter** (Priority 300) + - Logs complete incoming response details + - Includes status codes, headers, timing + - Correlates responses with requests + +**Test Basic Filter Functionality:** + +[source,bash] +---- +# Get all products with filters (watch server logs) +curl http://localhost:9050/payment/catalog/filtered/products + +# Expected in logs: +# 1. "Bearer token authentication added to request" (if token configured) +# 2. "Generated new Correlation ID: " +# 3. "=== Outgoing REST Client Request ===" +# 4. "Method: GET" +# 5. "URI: http://localhost:5050/catalog/api/products" +# 6. "=== Incoming REST Client Response ===" +# 7. "Status: 200 OK" +---- + +**Test Filter Execution with Specific Product:** + +[source,bash] +---- +# Get specific product - observe filter chain execution +curl http://localhost:9050/payment/catalog/filtered/products/1 + +# Expected Response: +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "method": "REST Client with Custom Filters", + "filterChain": "BearerToken → CorrelationId → RequestLogging → [HTTP] → ResponseLogging", + "note": "Check server logs for X-Correlation-ID and detailed request/response traces" +} +---- + +**Test Filters with Error Responses:** + +[source,bash] +---- +# Test 404 error - filters still execute and log the error +curl http://localhost:9050/payment/catalog/filtered/products/999999 + +# Expected in logs: +# - RequestLoggingFilter logs the outgoing request +# - ResponseLoggingFilter logs: "Status: 404 Not Found" +# - ResponseLoggingFilter logs: "Result: CLIENT ERROR (404)" +# - ProductNotFoundException is thrown after logging + +# Check product availability (handles 404 gracefully) +curl http://localhost:9050/payment/catalog/filtered/products/999999/available + +# Expected Response: +{ + "productId": 999999, + "available": false, + "method": "REST Client with Custom Filters", + "note": "Product not found - ResponseLoggingFilter logged 404 before returning false" +} +---- + +**Compare Filtered vs Non-Filtered Clients:** + +[source,bash] +---- +# Side-by-side comparison endpoint +curl http://localhost:9050/payment/catalog/compare/1 + +# This endpoint calls BOTH clients: +# 1. Non-filtered client (minimal logging) +# 2. Filtered client (comprehensive logging) + +# Compare the server logs to see the difference in observability +---- + +**Expected Log Output (Filtered Client):** + +[source,log] +---- +INFO: Getting product with custom filters: 1 +INFO: Bearer token authentication added to request +INFO: Generated new Correlation ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: === Outgoing REST Client Request === +INFO: Method: GET +INFO: URI: http://localhost:5050/catalog/api/products/1 +INFO: Request Headers: +INFO: Authorization: [REDACTED] +INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: X-Request-ID: b2c3d4e5-f6a7-8901-bcde-f12345678901 +INFO: =================================== +INFO: === Incoming REST Client Response === +INFO: Request: GET http://localhost:5050/catalog/api/products/1 +INFO: Status: 200 OK +INFO: Content-Type: application/json +INFO: X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +INFO: Result: SUCCESS +INFO: ==================================== +INFO: Successfully retrieved product with filters: Laptop +---- + +**Configure Bearer Token (Optional):** + +Edit `src/main/resources/META-INF/microprofile-config.properties`: + +[source,properties] +---- +# Uncomment and set your Bearer token +catalog-service.bearer.token=your-test-token-here +---- + +Restart the server and test again - you'll see the Authorization header added. + +**Verify Filter Priority:** + +Filter execution order is determined by `@Priority` values (lower numbers execute first): + +[source] +---- +Request Flow: +1. BearerTokenFilter (Priority 1000 - AUTHENTICATION) +2. CorrelationIdFilter (Priority 100) +3. RequestLoggingFilter (Priority 300) +4. --- HTTP REQUEST SENT --- +5. --- HTTP RESPONSE RECEIVED --- +6. ResponseLoggingFilter (Priority 300) +---- + +**Files to Review:** + +* [BearerTokenFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java) - Authentication filter +* [CorrelationIdFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java) - Distributed tracing +* [RequestLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java) - Request logging +* [ResponseLoggingFilter.java](src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java) - Response logging +* [ProductClientWithFilters.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java) - Client with filters registered + +**Benefits of Using Filters:** + +[source,bash] +---- +# Test the comparison endpoint to see benefits +curl http://localhost:9050/payment/catalog/compare/1 + +# Response shows benefits: +{ + "comparison": { + "noFilters": "Minimal logging - only business logic logs", + "withFilters": "Comprehensive logging - authentication, tracing, request/response details" + }, + "filterBenefits": [ + "Authentication without changing business code", + "Distributed tracing with correlation IDs", + "Complete request/response visibility", + "Easier debugging and monitoring", + "Cross-cutting concerns separated from business logic" + ] +} +---- + +**Common Use Cases for Filters:** + +* **Authentication**: Inject API keys, Bearer tokens, Basic Auth credentials +* **Tracing**: Propagate correlation IDs, request IDs, span IDs across services +* **Logging**: Debug requests/responses without modifying business logic +* **Headers**: Add custom headers, propagate context from incoming requests +* **Monitoring**: Collect metrics, measure latency, track error rates +* **Security**: Sign requests, validate responses, enforce policies + +**Security Best Practices:** + +1. Never log sensitive data (passwords, tokens, PII) in production +2. Use [REDACTED] masks for sensitive headers in logs +3. Store tokens in configuration, not hardcoded +4. Use environment-specific configuration for different deployments +5. Implement proper error handling in filters + +=== Example 7: RestClientBuilder for Programmatic Client Creation + +This example demonstrates creating REST clients programmatically without CDI injection. + +**What to Verify:** + +* Creating clients with `RestClientBuilder.newBuilder()` +* Using `baseUri(String)` method (MicroProfile Rest Client 4.0) +* Try-with-resources pattern with `AutoCloseable` +* Dynamic configuration from MicroProfile Config +* Environment-specific client configuration + +**Test Basic RestClientBuilder:** + +[source,bash] +---- +# Check product availability with programmatic client +curl http://localhost:9050/payment/catalog/builder/products/1/check + +# Expected Response: +{ + "productId": 1, + "available": true, + "method": "RestClientBuilder (programmatic)", + "note": "Product found using programmatically created client" +} +---- + +**Test Dynamic Configuration:** + +[source,bash] +---- +# Get product with configuration loaded from microprofile-config.properties +curl http://localhost:9050/payment/catalog/builder/products/1/dynamic + +# Expected Response: +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "method": "RestClientBuilder with dynamic config", + "note": "Configuration loaded from microprofile-config.properties at runtime" +} +---- + +**Test Environment-Specific Configuration:** + +[source,bash] +---- +# Get product for development environment +curl http://localhost:9050/payment/catalog/builder/products/1/env/dev + +# Get product for staging environment (will fail if service not available) +curl http://localhost:9050/payment/catalog/builder/products/1/env/staging + +# Get product for production environment (will fail if service not available) +curl http://localhost:9050/payment/catalog/builder/products/1/env/prod + +# Expected Response (dev): +{ + "product": { + "id": 1, + "name": "Laptop", + "price": 999.99 + }, + "environment": "dev", + "method": "RestClientBuilder with environment-specific config", + "note": "Client configured for dev environment" +} +---- + +**Test Batch Processing:** + +[source,bash] +---- +# Check multiple products in batch +curl -X POST http://localhost:9050/payment/catalog/builder/products/batch-check \ + -H "Content-Type: application/json" \ + -d '{"productIds": [1, 2, 3, 999, 1000]}' + +# Expected Response: +{ + "totalChecked": 5, + "availableCount": 3, + "unavailableCount": 2, + "method": "RestClientBuilder (batch processing)", + "note": "Each product checked with a separate programmatically created client instance" +} +---- + +**When to Use RestClientBuilder:** + +* CDI is unavailable or not desired +* Client configuration must be determined dynamically at runtime +* Creating multiple clients with different configurations +* Utility methods or batch jobs +* Testing environments where CDI injection is complex + +**Files to Review:** + +* [ProductClientBuilderService.java](src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java) - Programmatic client creation examples +* [ProductClient.java](src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java) - Note `extends AutoCloseable` + +=== Learning Objective + +After completing these examples, you should understand: + +- How to define a REST client interface with `@RegisterRestClient` +- How to configure REST clients using MicroProfile Config properties +- How to inject REST clients with `@Inject` and `@RestClient` in CDI beans +- How to use parameter annotations (`@PathParam`, `@QueryParam`, `@HeaderParam`) +- How to implement `ResponseExceptionMapper` for custom error handling +- The difference between checked and unchecked exceptions in exception mappers +- How to register exception mappers with `@RegisterProvider` +- How to extract error messages from JSON response bodies +- How CDI bean discovery works with `beans.xml` +- How to test REST clients in a microservices environment +- How to implement custom `ClientRequestFilter` for request interception +- How to implement custom `ClientResponseFilter` for response logging +- How to use `@Priority` to control filter execution order +- How to register filters using `@RegisterProvider` annotation +- How to separate cross-cutting concerns (auth, logging, tracing) from business logic +- How to create REST clients programmatically using `RestClientBuilder` +- How to use the `baseUri(String)` method in MicroProfile Rest Client 4.0 +- When to use `RestClientBuilder` vs CDI injection +- How to use try-with-resources with `AutoCloseable` REST clients +- How to dynamically configure REST clients at runtime + +=== Troubleshooting Common Issues + +**Issue: "Unsatisfied dependencies for type ProductClient"** + +*Solution:* Ensure `beans.xml` exists in `src/main/webapp/WEB-INF/` with `bean-discovery-mode="all"` + +**Issue: "Connection refused" errors** + +*Solution:* Verify the catalog service is running on port 5050: +[source,bash] +---- +curl http://localhost:5050/catalog/api/products +---- + +**Issue: "Read timeout" errors** + +*Solution:* Increase timeout values in `microprofile-config.properties`: +[source,properties] +---- +catalog-service/mp-rest/readTimeout=10000 +---- + +**Issue: Exceptions not being mapped correctly** + +*Solution:* Verify `@RegisterProvider` is present on the client interface and the mapper implements `ResponseExceptionMapper` + +== Project Structure + +[source] +---- +liberty-rest-app/ +├── user/ # User management service +├── inventory/ # Inventory management service +├── order/ # Order management service +└── catalog/ # Product catalog service +---- + +Each service follows a similar internal structure: + +[source] +---- +service/ +├── src/ +│ ├── main/ +│ │ ├── java/ # Java source code +│ │ ├── liberty/ # Liberty server configuration +│ │ └── webapp/ # Web resources +│ └── test/ # Test code +└── pom.xml # Maven configuration +---- + +== Key MicroProfile Features Demonstrated + +* *Config*: Externalized configuration +* *Fault Tolerance*: Circuit breakers, retries, fallbacks +* *Health Checks*: Application health monitoring +* *Metrics*: Performance monitoring +* *OpenAPI*: API documentation +* *Rest Client*: Type-safe REST clients diff --git a/code/chapter11/catalog/README.adoc b/code/chapter11/catalog/README.adoc new file mode 100644 index 00000000..cffee0be --- /dev/null +++ b/code/chapter11/catalog/README.adoc @@ -0,0 +1,622 @@ += MicroProfile Catalog Service +:toc: macro +:toclevels: 3 +:icons: font +:source-highlighter: highlight.js +:experimental: + +toc::[] + +== Overview + +The MicroProfile Catalog Service is a modern Jakarta EE 10 application built with MicroProfile 6.1 specifications and running on Open Liberty. This service provides a RESTful API for product catalog management with enhanced MicroProfile features. + +This project demonstrates the key capabilities of MicroProfile OpenAPI and in-memory persistence architecture. + +== Features + +* *RESTful API* using Jakarta RESTful Web Services +* *OpenAPI Documentation* with Swagger UI +* *In-Memory Persistence* using ConcurrentHashMap for thread-safe data storage +* *HTML Landing Page* with API documentation and service status +* *Maintenance Mode* support with configuration-based toggles + +== MicroProfile Features Implemented + +=== MicroProfile OpenAPI + +The application provides OpenAPI documentation for its REST endpoints. API documentation is generated automatically from annotations in the code: + +[source,java] +---- +@GET +@Produces(MediaType.APPLICATION_JSON) +@Operation(summary = "Get all products", description = "Returns a list of all products") +@APIResponses({ + @APIResponse(responseCode = "200", description = "List of products", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = Product.class))) +}) +public Response getAllProducts() { + // Implementation +} +---- + +The OpenAPI documentation is available at: `/openapi` (in various formats) and `/openapi/ui` (Swagger UI) + +=== In-Memory Persistence Architecture + +The application implements a thread-safe in-memory persistence layer using `ConcurrentHashMap`: + +[source,java] +---- +@ApplicationScoped +public class ProductRepository { + // In-memory storage using ConcurrentHashMap for thread safety + private final Map productsMap = new ConcurrentHashMap<>(); + + // ID generator + private final AtomicLong idGenerator = new AtomicLong(1); + + // CRUD operations... +} +---- + +==== Atomic ID Generation with AtomicLong + +The repository uses `java.util.concurrent.atomic.AtomicLong` for thread-safe ID generation: + +[source,java] +---- +// ID generation in createProduct method +if (product.getId() == null) { + product.setId(idGenerator.getAndIncrement()); +} +---- + +`AtomicLong` provides several key benefits: + +* *Thread Safety*: Guarantees atomic operations without explicit locking +* *Performance*: Uses efficient compare-and-swap (CAS) operations instead of locks +* *Consistency*: Ensures unique, sequential IDs even under concurrent access +* *No Synchronization*: Avoids the overhead of synchronized blocks + +===== Advanced AtomicLong Operations + +The ProductRepository implements an advanced pattern for handling both system-generated and client-provided IDs: + +[source,java] +---- +public Product createProduct(Product product) { + // Generate ID if not provided + if (product.getId() == null) { + product.setId(idGenerator.getAndIncrement()); + } else { + // Update idGenerator if the provided ID is greater than current + long nextId = product.getId() + 1; + while (true) { + long currentId = idGenerator.get(); + if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { + break; + } + } + } + + productsMap.put(product.getId(), product); + return product; +} +---- + +This implementation demonstrates several key AtomicLong patterns: + +1. *Initialization*: `AtomicLong` is initialized with a starting value of 1 to avoid using 0 as a valid ID +2. *getAndIncrement*: Atomically returns the current value and increments it in one operation +3. *compareAndSet*: Safely updates the ID generator if a client provides a higher ID value, preventing ID collisions +4. *Retry Logic*: Uses a spinlock pattern for handling concurrent updates to the AtomicLong when needed + +The initialization of the idGenerator with a specific starting value ensures the IDs begin at a predictable value: + +[source,java] +---- +private final AtomicLong idGenerator = new AtomicLong(1); // Start IDs at 1 +---- + +This approach ensures that each product receives a unique ID without risk of duplicate IDs in a concurrent environment. + +Key benefits of this in-memory persistence approach: + +* *Simplicity*: No need for database configuration or ORM mapping +* *Performance*: Fast in-memory access without network or disk I/O +* *Thread Safety*: ConcurrentHashMap provides thread-safe operations without blocking +* *Scalability*: Suitable for containerized deployments + +==== Thread Safety Implementation Details + +The implementation ensures thread safety through multiple mechanisms: + +1. *ConcurrentHashMap*: Uses lock striping to allow concurrent reads and thread-safe writes +2. *AtomicLong*: Provides atomic operations for ID generation +3. *Immutable Returns*: Returns new collections rather than internal references: ++ +[source,java] +---- +// Returns a copy of the collection to prevent concurrent modification issues +public List findAllProducts() { + return new ArrayList<>(productsMap.values()); +} +---- + +4. *Atomic Operations*: Uses atomic map operations like `putIfAbsent` and `compute` where appropriate + +NOTE: This implementation is suitable for development, testing, and scenarios where persistence across restarts is not required. + +=== MicroProfile Config + +The application uses MicroProfile Config to externalize configuration: + +[source,properties] +---- +# Enable OpenAPI scanning +mp.openapi.scan=true + +# Maintenance mode configuration +product.maintenanceMode=false +product.maintenanceMessage=The product catalog service is currently in maintenance mode. Please try again later. +---- + +The maintenance mode configuration allows dynamic control of service availability: + +* `product.maintenanceMode` - When set to `true`, the service returns a 503 Service Unavailable response +* `product.maintenanceMessage` - Customizable message displayed when the service is in maintenance mode + +==== Maintenance Mode Implementation + +The service checks the maintenance mode configuration before processing requests: + +[source,java] +---- +@Inject +@ConfigProperty(name="product.maintenanceMode", defaultValue="false") +private boolean maintenanceMode; + +@Inject +@ConfigProperty(name="product.maintenanceMessage", + defaultValue="The product catalog service is currently in maintenance mode. Please try again later.") +private String maintenanceMessage; + +// In request handling method +if (maintenance.isMaintenanceMode()) { + return Response + .status(Response.Status.SERVICE_UNAVAILABLE) + .entity(maintenance.getMaintenanceMessage()) + .build(); +} +---- + +This pattern enables: + +* Graceful service degradation during maintenance periods +* Dynamic control without redeployment (when using external configuration sources) +* Clear communication to API consumers + +== Architecture + +The application follows a layered architecture pattern: + +* *REST Layer* (`ProductResource`) - Handles HTTP requests and responses +* *Service Layer* (`ProductService`) - Contains business logic +* *Repository Layer* (`ProductRepository`) - Manages data access with in-memory storage +* *Model Layer* (`Product`) - Represents the business entities + +=== Persistence Evolution + +This application originally used JPA with Derby for persistence, but has been refactored to use an in-memory implementation: + +[cols="1,1", options="header"] +|=== +| Original JPA/Derby | Current In-Memory Implementation +| Required database configuration | No database configuration needed +| Persistence across restarts | Data reset on restart +| Used EntityManager and transactions | Uses ConcurrentHashMap and AtomicLong +| Required datasource in server.xml | No datasource configuration required +| Complex error handling | Simplified error handling +|=== + +Key architectural benefits of this change: + +* *Simplified Deployment*: No external database required +* *Faster Startup*: No database initialization delay +* *Reduced Dependencies*: Fewer libraries and configurations +* *Easier Testing*: No test database setup needed +* *Consistent Development Environment*: Same behavior across all development machines + +=== Containerization with Docker + +The application can be packaged into a Docker container: + +[source,bash] +---- +# Build the application +mvn clean package + +# Build the Docker image +docker build -t catalog-service . + +# Run the container +docker run -d -p 5050:5050 --name catalog-service catalog-service +---- + +==== AtomicLong in Containerized Environments + +When running the application in Docker or Kubernetes, some important considerations about AtomicLong behavior: + +1. *Per-Container State*: Each container has its own AtomicLong instance and state +2. *ID Collisions in Scaling*: When running multiple replicas, IDs are only unique within each container +3. *Persistence and Restarts*: AtomicLong resets on container restart, potentially causing ID reuse + +To handle these issues in production multi-container environments: + +* *External ID Generation*: Consider using a distributed ID generator service +* *Database Sequences*: For database implementations, use database sequences +* *Universally Unique IDs*: Consider UUIDs instead of sequential numeric IDs +* *Centralized Counter Service*: Use Redis or other distributed counter + +Example of adapting the code for distributed environments: + +[source,java] +---- +// Using UUIDs for distributed environments +private String generateId() { + return UUID.randomUUID().toString(); +} +---- + +== Development Workflow + +=== Running Locally + +To run the application in development mode: + +[source,bash] +---- +mvn clean liberty:dev +---- + +This starts the server in development mode, which: + +* Automatically deploys your code changes +* Provides hot reload capability +* Enables a debugger on port 7777 + +== Project Structure + +[source] +---- +catalog/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── io/microprofile/tutorial/store/ +│ │ │ └── product/ +│ │ │ ├── entity/ # Domain entities +│ │ │ ├── resource/ # REST resources +│ │ │ └── ProductRestApplication.java +│ │ ├── liberty/ +│ │ │ └── config/ +│ │ │ └── server.xml # Liberty server configuration +│ │ ├── resources/ +│ │ │ └── META-INF/ +│ │ │ └── microprofile-config.properties +│ │ └── webapp/ # Web resources +│ │ ├── index.html # Landing page with API documentation +│ │ └── WEB-INF/ +│ │ └── web.xml # Web application configuration +│ └── test/ # Test classes +└── pom.xml # Maven build file +---- + +== Getting Started + +=== Prerequisites + +* JDK 17+ +* Maven 3.8+ + +=== Building and Running + +To build and run the application: + +[source,bash] +---- +# Clone the repository +git clone https://github.com/yourusername/liberty-rest-app.git +cd code/catalog + +# Build the application +mvn clean package + +# Run the application +mvn liberty:run +---- + +=== Testing the Application + +==== Testing MicroProfile Features + +[source,bash] +---- +# OpenAPI documentation +curl -X GET http://localhost:5050/openapi + +# Check if service is in maintenance mode +curl -X GET http://localhost:5050/api/products +---- + +To view the Swagger UI, open the following URL in your browser: +http://localhost:5050/openapi/ui + +To view the landing page with API documentation: +http://localhost:5050/ + +== Server Configuration + +The application uses the following Liberty server configuration: + +[source,xml] +---- + + + jakartaEE-10.0 + microProfile-6.1 + restfulWS + jsonp + jsonb + cdi + mpConfig + mpOpenAPI + + + + + +---- + +== Development + +=== Adding a New Endpoint + +To add a new endpoint: + +1. Create a new method in the `ProductResource` class +2. Add appropriate Jakarta Restful Web Service annotations +3. Add OpenAPI annotations for documentation +4. Implement the business logic + +Example: + +[source,java] +---- +@GET +@Path("/search") +@Produces(MediaType.APPLICATION_JSON) +@Operation(summary = "Search products", description = "Search products by name") +@APIResponses({ + @APIResponse(responseCode = "200", description = "Products matching search criteria") +}) +public Response searchProducts(@QueryParam("name") String name) { + List matchingProducts = products.stream() + .filter(p -> p.getName().toLowerCase().contains(name.toLowerCase())) + .collect(Collectors.toList()); + return Response.ok(matchingProducts).build(); +} +---- + +=== Performance Considerations + +The in-memory data store provides excellent performance for read operations, but there are important considerations: + +* *Memory Usage*: Large data sets may consume significant memory +* *Persistence*: Data is lost when the application restarts +* *Scalability*: In a multi-instance deployment, each instance will have its own data store + +For production scenarios requiring data persistence, consider: + +1. Adding a database layer (PostgreSQL, MongoDB, etc.) +2. Implementing a distributed cache (Hazelcast, Redis, etc.) +3. Adding data synchronization between instances + +=== Concurrency Implementation Details + +==== AtomicLong vs Synchronized Counter + +The repository uses `AtomicLong` rather than traditional synchronized counters: + +[cols="1,1", options="header"] +|=== +| Traditional Approach | AtomicLong Approach +| `private long counter = 0;` | `private final AtomicLong idGenerator = new AtomicLong(1);` +| `synchronized long getNextId() { return ++counter; }` | `long nextId = idGenerator.getAndIncrement();` +| Locks entire method | Lock-free operation +| Subject to contention | Uses CPU compare-and-swap +| Performance degrades with multiple threads | Maintains performance under concurrency +|=== + +==== AtomicLong vs Other Concurrency Options + +[cols="1,1,1,1", options="header"] +|=== +| Feature | AtomicLong | Synchronized | java.util.concurrent.locks.Lock +| Type | Non-blocking | Intrinsic lock | Explicit lock +| Granularity | Single variable | Method/block | Customizable +| Performance under contention | High | Lower | Medium +| Visibility guarantee | Yes | Yes | Yes +| Atomicity guarantee | Yes | Yes | Yes +| Fairness policy | No | No | Optional +| Try/timeout support | Yes (compareAndSet) | No | Yes +| Multiple operations atomicity | Limited | Yes | Yes +| Implementation complexity | Simple | Simple | Complex +|=== + +===== When to Choose AtomicLong + +* *High-Contention Scenarios*: When many threads need to access/modify a counter +* *Single Variable Operations*: When only one variable needs atomic operations +* *Performance-Critical Code*: When minimizing lock contention is essential +* *Read-Heavy Workloads*: When reads significantly outnumber writes + +For this in-memory product repository, AtomicLong provides an optimal balance of safety and performance. + +==== Implementation in createProduct Method + +The ID generation logic handles both automatic and manual ID assignment: + +[source,java] +---- +public Product createProduct(Product product) { + // Generate ID if not provided + if (product.getId() == null) { + product.setId(idGenerator.getAndIncrement()); + } else { + // Update idGenerator if the provided ID is greater than current + long nextId = product.getId() + 1; + while (true) { + long currentId = idGenerator.get(); + if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { + break; + } + } + } + + productsMap.put(product.getId(), product); + return product; +} +---- + +This implementation ensures ID integrity while supporting both system-generated and client-provided IDs. + +This enables scanning of OpenAPI annotations in the application. + +== Troubleshooting + +=== Common Issues + +* *OpenAPI documentation not available*: Make sure `mp.openapi.scan=true` is set in the properties file +* *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations +* *Service always in maintenance mode*: Check the `product.maintenanceMode` property in `microprofile-config.properties` +* *API returning 503 responses*: The service is likely in maintenance mode; set `product.maintenanceMode=false` in configuration +* *OpenAPI documentation not available*: Make sure `mp.openapi.scan=true` is set in the properties file +* *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations + +=== Thread Safety Troubleshooting + +If experiencing concurrency issues: + +1. *Verify AtomicLong Usage*: Ensure all ID generation uses `AtomicLong.getAndIncrement()` instead of manual increment +2. *Check Collection Returns*: Always return copies of collections, not direct references: ++ +[source,java] +---- +public List findAllProducts() { + return new ArrayList<>(productsMap.values()); // Correct: returns a new copy +} +---- + +3. *Use ConcurrentHashMap Methods*: Prefer atomic methods like `compute`, `computeIfAbsent`, or `computeIfPresent` for complex operations +4. *Avoid Iteration + Modification*: Don't modify the map while iterating over it + +=== Understanding AtomicLong Internals + +If you need to debug issues with AtomicLong, understanding its internal mechanisms is helpful: + +==== Compare-And-Swap Operation + +AtomicLong relies on hardware-level atomic instructions, specifically Compare-And-Swap (CAS): + +[source,text] +---- +function CAS(address, expected, new): + atomically: + if memory[address] == expected: + memory[address] = new + return true + else: + return false +---- + +The implementation of `getAndIncrement()` uses this mechanism: + +[source,java] +---- +// Simplified implementation of getAndIncrement +public long getAndIncrement() { + while (true) { + long current = get(); + long next = current + 1; + if (compareAndSet(current, next)) + return current; + } +} +---- + +==== Memory Ordering and Visibility + +AtomicLong ensures that memory visibility follows the Java Memory Model: + +* All writes to the AtomicLong by one thread are visible to reads from other threads +* Memory barriers are established when performing atomic operations +* Volatile semantics are guaranteed without using the volatile keyword + +==== Diagnosing AtomicLong Issues + +1. *Unexpected ID Values*: Check for manual ID assignment bypassing the AtomicLong +2. *Duplicate IDs*: Verify the initialization value and ensure all ID assignments go through AtomicLong +3. *Performance Issues*: Look for excessive contention (many threads updating simultaneously) + +=== Logs + +Server logs can be found at: + +[source] +---- +target/liberty/wlp/usr/servers/defaultServer/logs/ +---- + +== Resources + +* https://microprofile.io/[MicroProfile] + +=== HTML Landing Page + +The application includes a user-friendly HTML landing page (`index.html`) that provides: + +* Service overview with comprehensive documentation +* API endpoints documentation with methods and descriptions +* Interactive examples for all API operations +* Links to OpenAPI/Swagger documentation + +==== Maintenance Mode Configuration in the UI + +The index.html page is designed to work seamlessly with the maintenance mode configuration. When maintenance mode is enabled via the `product.maintenanceMode` property, all API endpoints return a 503 Service Unavailable response with the configured maintenance message. + +The landing page displays comprehensive documentation about the API regardless of the maintenance state, allowing developers to continue learning about the API even when the service is undergoing maintenance. + +Key features of the landing page: + +* *Responsive Design*: Works well on desktop and mobile devices +* *Comprehensive API Documentation*: All endpoints with sample requests and responses +* *Interactive Examples*: Detailed sample requests and responses for each endpoint +* *Modern Styling*: Clean, professional appearance with card-based layout + +The landing page is configured as the welcome file in `web.xml`: + +[source,xml] +---- + + index.html + +---- + +This provides a user-friendly entry point for API consumers and developers. + + diff --git a/code/chapter11/catalog/pom.xml b/code/chapter11/catalog/pom.xml new file mode 100644 index 00000000..8f1917b3 --- /dev/null +++ b/code/chapter11/catalog/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + io.microprofile.tutorial + catalog + 1.0-SNAPSHOT + war + + + + + 21 + 21 + + UTF-8 + UTF-8 + + + 5050 + 5051 + + catalog + + + + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + jakarta.platform + jakarta.jakartaee-api + 10.0.0 + provided + + + + + org.eclipse.microprofile + microprofile + 7.1 + pom + provided + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + + io.openliberty.tools + liberty-maven-plugin + 3.11.2 + + mpServer + + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + + \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java new file mode 100644 index 00000000..9759e1f7 --- /dev/null +++ b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/ProductRestApplication.java @@ -0,0 +1,9 @@ +package io.microprofile.tutorial.store.product; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/api") +public class ProductRestApplication extends Application { + // No additional configuration is needed here +} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java new file mode 100644 index 00000000..84e3b23e --- /dev/null +++ b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/entity/Product.java @@ -0,0 +1,16 @@ +package io.microprofile.tutorial.store.product.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Product { + + private Long id; + private String name; + private String description; + private Double price; +} diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java new file mode 100644 index 00000000..6631fdea --- /dev/null +++ b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/repository/ProductRepository.java @@ -0,0 +1,138 @@ +package io.microprofile.tutorial.store.product.repository; + +import io.microprofile.tutorial.store.product.entity.Product; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Repository class for Product entity. + * Provides in-memory persistence operations using ConcurrentHashMap. + */ +@ApplicationScoped +public class ProductRepository { + + private static final Logger LOGGER = Logger.getLogger(ProductRepository.class.getName()); + + // In-memory storage using ConcurrentHashMap for thread safety + private final Map productsMap = new ConcurrentHashMap<>(); + + // ID generator + private final AtomicLong idGenerator = new AtomicLong(1); + + /** + * Constructor with sample data initialization. + */ + public ProductRepository() { + // Initialize with sample products + createProduct(new Product(null, "iPhone", "Apple iPhone 15", 999.99)); + createProduct(new Product(null, "MacBook", "Apple MacBook Air", 1299.0)); + createProduct(new Product(null, "iPad", "Apple iPad Pro", 799.0)); + LOGGER.info("ProductRepository initialized with sample products"); + } + + /** + * Retrieves all products. + * + * @return List of all products + */ + public List findAllProducts() { + LOGGER.fine("Repository: Finding all products"); + return new ArrayList<>(productsMap.values()); + } + + /** + * Retrieves a product by ID. + * + * @param id Product ID + * @return The product or null if not found + */ + public Product findProductById(Long id) { + LOGGER.fine("Repository: Finding product with ID: " + id); + return productsMap.get(id); + } + + /** + * Creates a new product. + * + * @param product Product data to create + * @return The created product with ID + */ + public Product createProduct(Product product) { + // Generate ID if not provided + if (product.getId() == null) { + product.setId(idGenerator.getAndIncrement()); + } else { + // Update idGenerator if the provided ID is greater than current + long nextId = product.getId() + 1; + while (true) { + long currentId = idGenerator.get(); + if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { + break; + } + } + } + + LOGGER.fine("Repository: Creating product with ID: " + product.getId()); + productsMap.put(product.getId(), product); + return product; + } + + /** + * Updates an existing product. + * + * @param product Updated product data + * @return The updated product or null if not found + */ + public Product updateProduct(Product product) { + Long id = product.getId(); + if (id != null && productsMap.containsKey(id)) { + LOGGER.fine("Repository: Updating product with ID: " + id); + productsMap.put(id, product); + return product; + } + LOGGER.warning("Repository: Product not found for update, ID: " + id); + return null; + } + + /** + * Deletes a product by ID. + * + * @param id ID of the product to delete + * @return true if deleted, false if not found + */ + public boolean deleteProduct(Long id) { + if (productsMap.containsKey(id)) { + LOGGER.fine("Repository: Deleting product with ID: " + id); + productsMap.remove(id); + return true; + } + LOGGER.warning("Repository: Product not found for deletion, ID: " + id); + return false; + } + + /** + * Searches for products by criteria. + * + * @param name Product name (optional) + * @param description Product description (optional) + * @param minPrice Minimum price (optional) + * @param maxPrice Maximum price (optional) + * @return List of matching products + */ + public List searchProducts(String name, String description, Double minPrice, Double maxPrice) { + LOGGER.fine("Repository: Searching for products with criteria"); + + return productsMap.values().stream() + .filter(p -> name == null || p.getName().toLowerCase().contains(name.toLowerCase())) + .filter(p -> description == null || p.getDescription().toLowerCase().contains(description.toLowerCase())) + .filter(p -> minPrice == null || p.getPrice() >= minPrice) + .filter(p -> maxPrice == null || p.getPrice() <= maxPrice) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java new file mode 100644 index 00000000..316ac882 --- /dev/null +++ b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/resource/ProductResource.java @@ -0,0 +1,182 @@ +package io.microprofile.tutorial.store.product.resource; + +import java.util.List; +import java.util.logging.Logger; +import java.util.logging.Level; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.microprofile.tutorial.store.product.entity.Product; +import io.microprofile.tutorial.store.product.service.ProductService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@ApplicationScoped +@Path("/products") +@Tag(name = "Product Resource", description = "CRUD operations for products") +public class ProductResource { + + private static final Logger LOGGER = Logger.getLogger(ProductResource.class.getName()); + + @Inject + @ConfigProperty(name="product.maintenanceMode", defaultValue="false") + private boolean maintenanceMode; + + @Inject + private ProductService productService; + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "List all products", description = "Retrieves a list of all products") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Successful, list of products found", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = Product.class))), + @APIResponse( + responseCode = "400", + description = "Unsuccessful, no products found", + content = @Content(mediaType = "application/json") + ), + @APIResponse( + responseCode = "503", + description = "Service is under maintenance", + content = @Content(mediaType = "application/json") + ) + }) + public Response getAllProducts() { + LOGGER.log(Level.INFO, "REST: Fetching all products"); + List products = productService.findAllProducts(); + + if (maintenanceMode) { + return Response + .status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Service is under maintenance") + .build(); + } + + if (products != null && !products.isEmpty()) { + return Response + .status(Response.Status.OK) + .entity(products).build(); + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity("No products found") + .build(); + } + } + + @GET + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get product by ID", description = "Returns a product by its ID") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Product found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))), + @APIResponse(responseCode = "404", description = "Product not found"), + @APIResponse(responseCode = "503", description = "Service is under maintenance") + }) + public Response getProductById(@PathParam("id") Long id) { + LOGGER.log(Level.INFO, "REST: Fetching product with id: {0}", id); + + if (maintenanceMode) { + return Response + .status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Service is under maintenance") + .build(); + } + + Product product = productService.findProductById(id); + if (product != null) { + return Response.ok(product).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Create a new product", description = "Creates a new product") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Product created", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))) + }) + public Response createProduct(Product product) { + LOGGER.info("REST: Creating product: " + product); + Product createdProduct = productService.createProduct(product); + return Response.status(Response.Status.CREATED).entity(createdProduct).build(); + } + + @PUT + @Path("/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Update a product", description = "Updates an existing product by its ID") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Product updated", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))), + @APIResponse(responseCode = "404", description = "Product not found") + }) + public Response updateProduct(@PathParam("id") Long id, Product updatedProduct) { + LOGGER.info("REST: Updating product with id: " + id); + Product updated = productService.updateProduct(id, updatedProduct); + if (updated != null) { + return Response.ok(updated).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @DELETE + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Delete a product", description = "Deletes a product by its ID") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Product deleted"), + @APIResponse(responseCode = "404", description = "Product not found") + }) + public Response deleteProduct(@PathParam("id") Long id) { + LOGGER.info("REST: Deleting product with id: " + id); + boolean deleted = productService.deleteProduct(id); + if (deleted) { + return Response.noContent().build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @GET + @Path("/search") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Search products", description = "Search products by criteria") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Search results", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Product.class))) + }) + public Response searchProducts( + @QueryParam("name") String name, + @QueryParam("description") String description, + @QueryParam("minPrice") Double minPrice, + @QueryParam("maxPrice") Double maxPrice) { + LOGGER.info("REST: Searching products with criteria"); + List results = productService.searchProducts(name, description, minPrice, maxPrice); + return Response.ok(results).build(); + } +} \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java new file mode 100644 index 00000000..804fd920 --- /dev/null +++ b/code/chapter11/catalog/src/main/java/io/microprofile/tutorial/store/product/service/ProductService.java @@ -0,0 +1,97 @@ +package io.microprofile.tutorial.store.product.service; + +import io.microprofile.tutorial.store.product.entity.Product; +import io.microprofile.tutorial.store.product.repository.ProductRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import java.util.List; +import java.util.logging.Logger; + +/** + * Service class for Product operations. + * Contains business logic for product management. + */ +@RequestScoped +public class ProductService { + + private static final Logger LOGGER = Logger.getLogger(ProductService.class.getName()); + + @Inject + private ProductRepository repository; + + /** + * Retrieves all products. + * + * @return List of all products + */ + public List findAllProducts() { + LOGGER.info("Service: Finding all products"); + return repository.findAllProducts(); + } + + /** + * Retrieves a product by ID. + * + * @param id Product ID + * @return The product or null if not found + */ + public Product findProductById(Long id) { + LOGGER.info("Service: Finding product with ID: " + id); + return repository.findProductById(id); + } + + /** + * Creates a new product. + * + * @param product Product data to create + * @return The created product with ID + */ + public Product createProduct(Product product) { + LOGGER.info("Service: Creating new product: " + product); + return repository.createProduct(product); + } + + /** + * Updates an existing product. + * + * @param id ID of the product to update + * @param updatedProduct Updated product data + * @return The updated product or null if not found + */ + public Product updateProduct(Long id, Product updatedProduct) { + LOGGER.info("Service: Updating product with ID: " + id); + + Product existingProduct = repository.findProductById(id); + if (existingProduct != null) { + // Set the ID to ensure correct update + updatedProduct.setId(id); + return repository.updateProduct(updatedProduct); + } + return null; + } + + /** + * Deletes a product by ID. + * + * @param id ID of the product to delete + * @return true if deleted, false if not found + */ + public boolean deleteProduct(Long id) { + LOGGER.info("Service: Deleting product with ID: " + id); + return repository.deleteProduct(id); + } + + /** + * Searches for products by criteria. + * + * @param name Product name (optional) + * @param description Product description (optional) + * @param minPrice Minimum price (optional) + * @param maxPrice Maximum price (optional) + * @return List of matching products + */ + public List searchProducts(String name, String description, Double minPrice, Double maxPrice) { + LOGGER.info("Service: Searching for products with criteria"); + return repository.searchProducts(name, description, minPrice, maxPrice); + } +} diff --git a/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..03fbb4d9 --- /dev/null +++ b/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,5 @@ +# microprofile-config.properties +product.maintainenceMode=false + +# Enable OpenAPI scanning +mp.openapi.scan=true \ No newline at end of file diff --git a/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml b/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..10105161 --- /dev/null +++ b/code/chapter11/catalog/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + Product Catalog Service + + + index.html + + + diff --git a/code/chapter11/catalog/src/main/webapp/index.html b/code/chapter11/catalog/src/main/webapp/index.html new file mode 100644 index 00000000..54622a43 --- /dev/null +++ b/code/chapter11/catalog/src/main/webapp/index.html @@ -0,0 +1,281 @@ + + + + + + Product Catalog Service + + + +
+

Product Catalog Service

+

A microservice for managing product information in the e-commerce platform

+
+ +
+
+

Service Overview

+

The Product Catalog Service provides a REST API for managing product information, including:

+
    +
  • Creating new products
  • +
  • Retrieving product details
  • +
  • Updating existing products
  • +
  • Deleting products
  • +
  • Searching for products by various criteria
  • +
+
+

MicroProfile Config Implementation

+

This service implements configurability as per MicroProfile Config standards. Key configuration properties include:

+
    +
  • product.maintenanceMode - Controls whether the service is in maintenance mode (returns 503 responses)
  • +
  • mp.openapi.scan - Enables automatic OpenAPI documentation generation
  • +
+

MicroProfile Config allows these properties to be changed via environment variables, system properties, or configuration files without requiring application redeployment.

+
+
+ +
+

API Endpoints

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperationMethodURLDescription
List All ProductsGET/api/productsRetrieves a list of all products
Get Product by IDGET/api/products/{id}Returns a product by its ID
Create ProductPOST/api/productsCreates a new product
Update ProductPUT/api/products/{id}Updates an existing product by its ID
Delete ProductDELETE/api/products/{id}Deletes a product by its ID
Search ProductsGET/api/products/searchSearch products by criteria (name, description, price range)
+
+ +
+

API Documentation

+

The API is documented using MicroProfile OpenAPI. You can access the Swagger UI at:

+

/openapi/ui

+

The OpenAPI definition is available at:

+

/openapi

+
+ +
+

Sample Usage

+ +

List All Products

+
GET /api/products
+

Response:

+
[
+  {
+    "id": 1,
+    "name": "Smartphone X",
+    "description": "Latest smartphone with advanced features",
+    "price": 799.99
+  },
+  {
+    "id": 2,
+    "name": "Laptop Pro",
+    "description": "High-performance laptop for professionals",
+    "price": 1299.99
+  }
+]
+ +

Get Product by ID

+
GET /api/products/1
+

Response:

+
{
+  "id": 1,
+  "name": "Smartphone X",
+  "description": "Latest smartphone with advanced features",
+  "price": 799.99
+}
+ +

Create a New Product

+
POST /api/products
+Content-Type: application/json
+
+{
+  "name": "Wireless Earbuds",
+  "description": "Premium wireless earbuds with noise cancellation",
+  "price": 149.99
+}
+

Response:

+
{
+  "id": 3,
+  "name": "Wireless Earbuds",
+  "description": "Premium wireless earbuds with noise cancellation",
+  "price": 149.99
+}
+ +

Update a Product

+
PUT /api/products/3
+Content-Type: application/json
+
+{
+  "name": "Wireless Earbuds Pro",
+  "description": "Premium wireless earbuds with advanced noise cancellation",
+  "price": 179.99
+}
+

Response:

+
{
+  "id": 3,
+  "name": "Wireless Earbuds Pro",
+  "description": "Premium wireless earbuds with advanced noise cancellation",
+  "price": 179.99
+}
+ +

Delete a Product

+
DELETE /api/products/3
+

Response: No content (204)

+ +

Search for Products

+
GET /api/products/search?name=laptop&minPrice=1000&maxPrice=2000
+

Response:

+
[
+  {
+    "id": 2,
+    "name": "Laptop Pro",
+    "description": "High-performance laptop for professionals",
+    "price": 1299.99
+  }
+]
+
+
+ +
+

Product Catalog Service

+

© 2025 - MicroProfile APT Tutorial

+
+ + + + diff --git a/code/chapter11/docker-compose.yml b/code/chapter11/docker-compose.yml new file mode 100644 index 00000000..c52b6af0 --- /dev/null +++ b/code/chapter11/docker-compose.yml @@ -0,0 +1,85 @@ +services: + user-service: + build: ./user + ports: + - "6050:6050" + - "6051:6051" + networks: + - ecommerce-network + environment: + - INVENTORY_SERVICE_URL=http://inventory-service:7050 + - ORDER_SERVICE_URL=http://order-service:8050 + - CATALOG_SERVICE_URL=http://catalog-service:9050 + + inventory-service: + build: ./inventory + ports: + - "7050:7050" + - "7051:7051" + networks: + - ecommerce-network + depends_on: + - user-service + + order-service: + build: ./order + ports: + - "8050:8050" + - "8051:8051" + networks: + - ecommerce-network + depends_on: + - user-service + - inventory-service + + catalog-service: + build: ./catalog + ports: + - "5050:5050" + - "5051:5051" + networks: + - ecommerce-network + depends_on: + - inventory-service + + payment-service: + build: ./payment + ports: + - "9050:9050" + - "9051:9051" + networks: + - ecommerce-network + depends_on: + - user-service + - order-service + + shoppingcart-service: + build: ./shoppingcart + ports: + - "4050:4050" + - "4051:4051" + networks: + - ecommerce-network + depends_on: + - inventory-service + - catalog-service + environment: + - INVENTORY_SERVICE_URL=http://inventory-service:7050 + - CATALOG_SERVICE_URL=http://catalog-service:5050 + + shipment-service: + build: ./shipment + ports: + - "8060:8060" + - "9060:9060" + networks: + - ecommerce-network + depends_on: + - order-service + environment: + - ORDER_SERVICE_URL=http://order-service:8050/order + - MP_CONFIG_PROFILE=docker + +networks: + ecommerce-network: + driver: bridge diff --git a/code/chapter11/inventory/README.adoc b/code/chapter11/inventory/README.adoc new file mode 100644 index 00000000..2df7fec7 --- /dev/null +++ b/code/chapter11/inventory/README.adoc @@ -0,0 +1,387 @@ += Inventory Service +:toc: left +:icons: font +:source-highlighter: highlightjs + +A comprehensive Jakarta EE and MicroProfile-based REST service for inventory management demonstrating advanced MicroProfile Rest Client integration patterns. + +== Overview + +The Inventory Service is a production-ready microservice built with Jakarta EE 10.0 and MicroProfile 6.1, showcasing comprehensive REST client integration patterns with the Catalog Service. This service demonstrates three different approaches to MicroProfile Rest Client usage: + +* **Injected REST Client** (`@RestClient`) for standard operations +* **RestClientBuilder** with custom timeouts for availability checks +* **Advanced RestClientBuilder** with fine-tuned configuration for detailed operations + +== Key Features + +=== Core Functionality +* Complete CRUD operations for inventory management +* Product validation against catalog service +* Inventory reservation system with availability checks +* Bulk operations support +* Enriched inventory data with product information +* Pagination and filtering capabilities + +=== MicroProfile Rest Client Integration +* **Three distinct REST client approaches** for different use cases +* **Product validation** before inventory operations +* **Service integration** with catalog service on port 5050 +* **Error handling** for non-existent products and service failures +* **Timeout configurations** optimized for different operation types + +=== Advanced Features +* Bean validation for input data +* Comprehensive exception handling +* Transaction management for atomic operations +* OpenAPI documentation with Swagger UI +* Health checks and service monitoring + +== Running the Application + +To start the application, run: + +[source,bash] +---- +cd inventory +mvn liberty:run +---- + +This will start the Open Liberty server on port 7050 (HTTP) and 7051 (HTTPS). + +== MicroProfile Rest Client Implementations + +=== 1. Injected REST Client (`@RestClient`) +Used for standard product validation operations: + +[source,java] +---- +@Inject +@RestClient +private ProductServiceClient productServiceClient; +---- + +**Configuration** (microprofile-config.properties): +[source,properties] +---- +product-service/mp-rest/url=http://localhost:5050/catalog/api +product-service/mp-rest/scope=jakarta.inject.Singleton +product-service/mp-rest/connectTimeout=5000 +product-service/mp-rest/readTimeout=10000 +---- + +=== 2. RestClientBuilder (5s/10s timeout) +Used for lightweight availability checks during reservation: + +[source,java] +---- +ProductServiceClient dynamicClient = RestClientBuilder.newBuilder() + .baseUri(catalogServiceUri) + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(ProductServiceClient.class); +---- + +=== 3. Advanced RestClientBuilder (3s/8s timeout) +Used for detailed product information retrieval: + +[source,java] +---- +ProductServiceClient customClient = RestClientBuilder.newBuilder() + .baseUri(catalogServiceUri) + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(8, TimeUnit.SECONDS) + .build(ProductServiceClient.class); +---- + +== Complete API Endpoints + +[cols="1,3,2,3", options="header"] +|=== +|Method |URL |MicroProfile Client |Description + +|GET +|/api/inventories +|None +|Get all inventory items with pagination/filtering + +|POST +|/api/inventories +|@RestClient +|Create new inventory (validates product exists) + +|GET +|/api/inventories/{id} +|None +|Get inventory by ID + +|PUT +|/api/inventories/{id} +|@RestClient +|Update inventory (validates product exists) + +|DELETE +|/api/inventories/{id} +|None +|Delete inventory + +|GET +|/api/inventories/product/{productId} +|None +|Get inventory by product ID + +|PATCH +|/api/inventories/product/{productId}/quantity/{quantity} +|None +|Update product quantity + +|PATCH +|/api/inventories/product/{productId}/reserve/{quantity} +|RestClientBuilder (5s/10s) +|Reserve inventory with availability check + +|GET +|/api/inventories/product-info/{productId} +|Advanced RestClientBuilder (3s/8s) +|Get product details using custom client + +|GET +|/api/inventories/{id}/with-product-info +|@RestClient +|Get enriched inventory with product information + +|POST +|/api/inventories/bulk +|@RestClient +|Bulk create inventories with validation +|=== + +== Service Integration + +=== Catalog Service Integration +The inventory service integrates with the catalog service running on port 5050 to: + +* **Validate products** before creating or updating inventory +* **Check product availability** during reservation operations +* **Enrich inventory data** with product details (name, description, price) +* **Handle service failures** gracefully with appropriate error responses + +=== Error Handling +* **404 responses** when products don't exist in catalog +* **Service timeout handling** with different timeout configurations per operation +* **Fallback behavior** for service communication failures +* **Validation errors** for invalid inventory data + +== Testing with cURL + +=== Basic Operations + +==== Get all inventory items +[source,bash] +---- +curl -X GET http://localhost:7050/inventory/api/inventories +---- + +==== Get inventory by ID +[source,bash] +---- +curl -X GET http://localhost:7050/inventory/api/inventories/1 +---- + +==== Create new inventory (with product validation) +[source,bash] +---- +curl -X POST http://localhost:7050/inventory/api/inventories \ + -H "Content-Type: application/json" \ + -d '{"productId": 1, "quantity": 50, "location": "Warehouse A"}' +---- + +==== Update inventory (with product validation) +[source,bash] +---- +curl -X PUT http://localhost:7050/inventory/api/inventories/1 \ + -H "Content-Type: application/json" \ + -d '{"productId": 1, "quantity": 75, "location": "Warehouse B"}' +---- + +==== Delete inventory +[source,bash] +---- +curl -X DELETE http://localhost:7050/inventory/api/inventories/1 +---- + +=== Advanced Operations + +==== Reserve inventory (uses RestClientBuilder) +[source,bash] +---- +curl -X PATCH http://localhost:7050/inventory/api/inventories/product/1/reserve/10 +---- + +==== Get product info (uses Advanced RestClientBuilder) +[source,bash] +---- +curl -X GET http://localhost:7050/inventory/api/inventories/product-info/1 +---- + +==== Get enriched inventory with product details +[source,bash] +---- +curl -X GET http://localhost:7050/inventory/api/inventories/1/with-product-info +---- + +==== Bulk create inventories +[source,bash] +---- +curl -X POST http://localhost:7050/inventory/api/inventories/bulk \ + -H "Content-Type: application/json" \ + -d '[ + {"productId": 1, "quantity": 50, "location": "Warehouse A"}, + {"productId": 2, "quantity": 30, "location": "Warehouse B"} + ]' +---- + +==== Get inventory with pagination and filtering +[source,bash] +---- + +# Filter by location +curl -X GET "http://localhost:6050/inventory/api/inventories?location=Warehouse%20A" + +# Filter by minimum quantity +curl -X GET "http://localhost:6050/inventory/api/inventories?minQuantity=10" +---- + +== Test Scripts + +Comprehensive test scripts are available to test all functionality: + +* **`test-inventory-endpoints.sh`** - Complete test suite covering all endpoints and MicroProfile Rest Client features +* **`quick-test-commands.sh`** - Quick reference commands for manual testing +* **`TEST-SCRIPTS-README.md`** - Detailed documentation of test scenarios and expected responses + +[source,bash] +---- +# Run comprehensive test suite +./test-inventory-endpoints.sh + +# View test documentation +cat TEST-SCRIPTS-README.md +---- + +== Configuration + +=== MicroProfile Config Properties + +**REST Client Configuration** (`microprofile-config.properties`): +[source,properties] +---- +# Injected REST Client configuration +product-service/mp-rest/url=http://localhost:5050/catalog/api +product-service/mp-rest/scope=jakarta.inject.Singleton +product-service/mp-rest/connectTimeout=5000 +product-service/mp-rest/readTimeout=10000 +product-service/mp-rest/followRedirects=true +---- + +**RestClientBuilder Configuration** (programmatic): +[source,java] +---- +# Availability check client (5s/10s timeout) +URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); + +# Product info client (3s/8s timeout) +URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); +---- + +== OpenAPI Documentation + +View the complete API documentation: + +* **Swagger UI**: http://localhost:7050/inventory/api/openapi-ui/ +* **OpenAPI JSON**: http://localhost:7050/inventory/api/openapi +* **Service Landing Page**: http://localhost:7050/inventory/ + +== Project Structure + +[source] +---- +inventory/ +├── src/ +│ └── main/ +│ ├── java/ # Java source files +│ │ └── io/microprofile/tutorial/store/inventory/ +│ │ ├── entity/ # Domain entities +│ │ ├── exception/ # Custom exceptions +│ │ ├── service/ # Business logic +│ │ └── resource/ # REST endpoints +│ ├── liberty/ +│ │ └── config/ # Liberty server configuration +│ └── webapp/ # Web resources +└── pom.xml # Project dependencies and build +---- + +== Exception Handling + +The service implements a robust exception handling mechanism: + +[cols="1,2", options="header"] +|=== +|Exception |Purpose + +|`InventoryNotFoundException` +|Thrown when requested inventory item does not exist (HTTP 404) + +|`InventoryConflictException` +|Thrown when attempting to create duplicate inventory (HTTP 409) +|=== + +Exceptions are handled globally using `@Provider`: + +[source,java] +---- +@Provider +public class InventoryExceptionMapper implements ExceptionMapper { + // Maps exceptions to appropriate HTTP responses +} +---- + +== Transaction Management + +The service includes the Jakarta Transactions feature (`transaction-1.3`) but does not use database persistence. In this context, `@Transactional` has limited use: + +* Can be used for transaction-like behavior in memory operations +* Useful when you need to ensure multiple operations are executed atomically +* Provides rollback capability for in-memory state changes +* Primarily used for maintaining consistency in distributed operations + +[NOTE] +==== +Since this service doesn't use database persistence, `@Transactional` mainly serves as a boundary for: + +* Coordinating multiple service method calls +* Managing concurrent access to shared resources +* Ensuring atomic operations across multiple steps +==== + +Example usage: + +[source,java] +---- +@ApplicationScoped +public class InventoryService { + private final ConcurrentHashMap inventoryStore; + + @Transactional + public void updateInventory(Long id, Inventory inventory) { + // Even without persistence, @Transactional can help manage + // atomic operations and coordinate multiple method calls + if (!inventoryStore.containsKey(id)) { + throw new InventoryNotFoundException(id); + } + // Multiple operations that need to be atomic + updateQuantity(id, inventory.getQuantity()); + notifyInventoryChange(id); + } +} +---- diff --git a/code/chapter11/inventory/pom.xml b/code/chapter11/inventory/pom.xml new file mode 100644 index 00000000..988b3fb3 --- /dev/null +++ b/code/chapter11/inventory/pom.xml @@ -0,0 +1,163 @@ + + + + 4.0.0 + + io.microprofile + inventory + 1.0-SNAPSHOT + war + + inventory-management + https://microprofile.io + + + UTF-8 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + 6050 + 6051 + + inventory + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + org.junit.jupiter + junit-jupiter-api + 5.9.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + + + + + org.glassfish.jersey.core + jersey-common + 3.1.1 + test + + + + + inventory + + + + io.openliberty.tools + liberty-maven-plugin + 3.11.2 + + inventoryServer + runnable + 120 + + /inventory + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java new file mode 100644 index 00000000..e3c98814 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/InventoryApplication.java @@ -0,0 +1,33 @@ +package io.microprofile.tutorial.store.inventory; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.info.Contact; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.License; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * JAX-RS application for inventory management. + */ +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Inventory API", + version = "1.0.0", + description = "API for managing product inventory", + license = @License( + name = "Eclipse Public License 2.0", + url = "https://www.eclipse.org/legal/epl-2.0/"), + contact = @Contact( + name = "Inventory API Support", + email = "support@example.com")), + tags = { + @Tag(name = "Inventory", description = "Operations related to product inventory management") + } +) +public class InventoryApplication extends Application { + // The resources will be discovered automatically +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java new file mode 100644 index 00000000..6aee3f2e --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/client/ProductServiceClient.java @@ -0,0 +1,23 @@ +package io.microprofile.tutorial.store.inventory.client; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import java.util.List; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.microprofile.tutorial.store.inventory.dto.Product; + +@RegisterRestClient(configKey = "product-service") +@Path("/products") +public interface ProductServiceClient extends AutoCloseable { + + @GET + @Path("/{id}") + Product getProductById(@PathParam("id") Long id); + + @GET + List getProductsByCategory(@QueryParam("category") String category); +} \ No newline at end of file diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java new file mode 100644 index 00000000..6aed1f88 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/InventoryWithProductInfo.java @@ -0,0 +1,165 @@ +package io.microprofile.tutorial.store.inventory.dto; + +import io.microprofile.tutorial.store.inventory.entity.Inventory; +import jakarta.json.bind.annotation.JsonbProperty; + +/** + * DTO that combines inventory data with product information from the catalog service. + */ +public class InventoryWithProductInfo { + + @JsonbProperty("inventory") + private Inventory inventory; + + @JsonbProperty("product") + private Product product; + + /** + * Default constructor for JSON-B. + */ + public InventoryWithProductInfo() { + } + + /** + * Constructor with parameters. + * + * @param inventory The inventory information + * @param product The product information from catalog service + */ + public InventoryWithProductInfo(Inventory inventory, Product product) { + this.inventory = inventory; + this.product = product; + } + + /** + * Gets the inventory information. + * + * @return The inventory + */ + public Inventory getInventory() { + return inventory; + } + + /** + * Sets the inventory information. + * + * @param inventory The inventory to set + */ + public void setInventory(Inventory inventory) { + this.inventory = inventory; + } + + /** + * Gets the product information. + * + * @return The product + */ + public Product getProduct() { + return product; + } + + /** + * Sets the product information. + * + * @param product The product to set + */ + public void setProduct(Product product) { + this.product = product; + } + + /** + * Gets the inventory ID. + * + * @return The inventory ID + */ + @JsonbProperty("inventoryId") + public Long getInventoryId() { + return inventory != null ? inventory.getInventoryId() : null; + } + + /** + * Gets the product ID. + * + * @return The product ID + */ + @JsonbProperty("productId") + public Long getProductId() { + return inventory != null ? inventory.getProductId() : null; + } + + /** + * Gets the product name. + * + * @return The product name + */ + @JsonbProperty("productName") + public String getProductName() { + return product != null ? product.getName() : null; + } + + /** + * Gets the product price. + * + * @return The product price + */ + @JsonbProperty("productPrice") + public Double getProductPrice() { + return product != null ? product.getPrice() : null; + } + + /** + * Gets the product category. + * + * @return The product category + */ + @JsonbProperty("productCategory") + public String getProductCategory() { + return product != null ? product.getCategory() : null; + } + + /** + * Gets the inventory quantity. + * + * @return The quantity + */ + @JsonbProperty("quantity") + public Integer getQuantity() { + return inventory != null ? inventory.getQuantity() : null; + } + + /** + * Gets the reserved quantity. + * + * @return The reserved quantity + */ + @JsonbProperty("reservedQuantity") + public Integer getReservedQuantity() { + return inventory != null ? inventory.getReservedQuantity() : null; + } + + /** + * Gets the available quantity (quantity - reserved). + * + * @return The available quantity + */ + @JsonbProperty("availableQuantity") + public Integer getAvailableQuantity() { + if (inventory == null) { + return null; + } + return inventory.getQuantity() - inventory.getReservedQuantity(); + } + + @Override + public String toString() { + return "InventoryWithProductInfo{" + + "inventoryId=" + getInventoryId() + + ", productId=" + getProductId() + + ", productName='" + getProductName() + '\'' + + ", quantity=" + getQuantity() + + ", availableQuantity=" + getAvailableQuantity() + + ", price=" + getProductPrice() + + ", category='" + getProductCategory() + '\'' + + '}'; + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java new file mode 100644 index 00000000..0e7c8618 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/dto/Product.java @@ -0,0 +1,69 @@ +package io.microprofile.tutorial.store.inventory.dto; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTransient; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Product DTO for the inventory service. + * This class represents product information received from the product service. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Product { + + /** + * Unique identifier for the product. + */ + private Long id; + + /** + * Name of the product. + */ + private String name; + + /** + * Price of the product. + */ + private Double price; + + /** + * Category of the product. + */ + private String category; + + /** + * Description of the product. + */ + private String description; + + /** + * Availability status of the product. + */ + @JsonbTransient + private boolean isAvailable = true; + + @JsonbCreator + public Product( + @JsonbProperty("id") Long id, + @JsonbProperty("name") String name, + @JsonbProperty("price") Double price, + @JsonbProperty("category") String category, + @JsonbProperty("description") String description) { + this.id = id; + this.name = name; + this.price = price; + this.category = category; + this.description = description; + } + + @Override + public String toString() { + return String.format("Product{id=%d, name='%s', price=%.2f, category='%s', isAvailable=%b}", + id, name, price, category, isAvailable); + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java new file mode 100644 index 00000000..6b7b9693 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/entity/Inventory.java @@ -0,0 +1,51 @@ +package io.microprofile.tutorial.store.inventory.entity; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Inventory class for the microprofile tutorial store application. + * This class represents inventory information for products in the system. + * We're using an in-memory data structure rather than a database. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Inventory { + + /** + * Unique identifier for the inventory record. + * This can be null for new records before they are persisted. + */ + private Long inventoryId; + + /** + * Reference to the product this inventory record belongs to. + * Must not be null to maintain data integrity. + */ + @NotNull(message = "Product ID cannot be null") + private Long productId; + + /** + * Current quantity of the product available in inventory. + * Must not be null and must be non-negative. + */ + @NotNull(message = "Quantity cannot be null") + @Min(value = 0, message = "Quantity must be greater than or equal to 0") + private Integer quantity; + + /** + * Quantity of the product that is reserved (e.g., in pending orders). + * Must not be null and must be non-negative. + */ + @NotNull(message = "Reserved quantity cannot be null") + @Min(value = 0, message = "Reserved quantity must be greater than or equal to 0") + @Builder.Default + private Integer reservedQuantity = 0; +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java new file mode 100644 index 00000000..c99ad4d4 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/ErrorResponse.java @@ -0,0 +1,103 @@ +package io.microprofile.tutorial.store.inventory.exception; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents an error response to be returned to the client. + * Used for formatting error messages in a consistent way. + */ +public class ErrorResponse { + private String errorCode; + private String message; + private Map details; + + /** + * Constructs a new ErrorResponse with the specified error code and message. + * + * @param errorCode a code identifying the error type + * @param message a human-readable error message + */ + public ErrorResponse(String errorCode, String message) { + this.errorCode = errorCode; + this.message = message; + this.details = new HashMap<>(); + } + + /** + * Constructs a new ErrorResponse with the specified error code, message, and details. + * + * @param errorCode a code identifying the error type + * @param message a human-readable error message + * @param details additional information about the error + */ + public ErrorResponse(String errorCode, String message, Map details) { + this.errorCode = errorCode; + this.message = message; + this.details = details; + } + + /** + * Gets the error code. + * + * @return the error code + */ + public String getErrorCode() { + return errorCode; + } + + /** + * Sets the error code. + * + * @param errorCode the error code to set + */ + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + /** + * Gets the error message. + * + * @return the error message + */ + public String getMessage() { + return message; + } + + /** + * Sets the error message. + * + * @param message the error message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the error details. + * + * @return the error details + */ + public Map getDetails() { + return details; + } + + /** + * Sets the error details. + * + * @param details the error details to set + */ + public void setDetails(Map details) { + this.details = details; + } + + /** + * Adds a detail to the error response. + * + * @param key the detail key + * @param value the detail value + */ + public void addDetail(String key, Object value) { + this.details.put(key, value); + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java new file mode 100644 index 00000000..22010349 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryConflictException.java @@ -0,0 +1,41 @@ +package io.microprofile.tutorial.store.inventory.exception; + +import jakarta.ws.rs.core.Response; + +/** + * Exception thrown when there is a conflict with an inventory operation, + * such as when trying to create an inventory for a product that already has one. + */ +public class InventoryConflictException extends RuntimeException { + private Response.Status status; + + /** + * Constructs a new InventoryConflictException with the specified message. + * + * @param message the detail message + */ + public InventoryConflictException(String message) { + super(message); + this.status = Response.Status.CONFLICT; + } + + /** + * Constructs a new InventoryConflictException with the specified message and status. + * + * @param message the detail message + * @param status the HTTP status code to return + */ + public InventoryConflictException(String message, Response.Status status) { + super(message); + this.status = status; + } + + /** + * Gets the HTTP status associated with this exception. + * + * @return the HTTP status + */ + public Response.Status getStatus() { + return status; + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java new file mode 100644 index 00000000..224062eb --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryExceptionMapper.java @@ -0,0 +1,46 @@ +package io.microprofile.tutorial.store.inventory.exception; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Exception mapper for handling all runtime exceptions in the inventory service. + * Maps exceptions to appropriate HTTP responses with formatted error messages. + */ +@Provider +public class InventoryExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = Logger.getLogger(InventoryExceptionMapper.class.getName()); + + @Override + public Response toResponse(RuntimeException exception) { + if (exception instanceof InventoryNotFoundException) { + InventoryNotFoundException notFoundException = (InventoryNotFoundException) exception; + LOGGER.log(Level.INFO, "Resource not found: {0}", exception.getMessage()); + + return Response.status(notFoundException.getStatus()) + .entity(new ErrorResponse("not_found", exception.getMessage())) + .type(MediaType.APPLICATION_JSON) + .build(); + } else if (exception instanceof InventoryConflictException) { + InventoryConflictException conflictException = (InventoryConflictException) exception; + LOGGER.log(Level.INFO, "Resource conflict: {0}", exception.getMessage()); + + return Response.status(conflictException.getStatus()) + .entity(new ErrorResponse("conflict", exception.getMessage())) + .type(MediaType.APPLICATION_JSON) + .build(); + } + + // Handle unexpected exceptions + LOGGER.log(Level.SEVERE, "Unexpected error", exception); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse("server_error", "An unexpected error occurred")) + .type(MediaType.APPLICATION_JSON) + .build(); + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java new file mode 100644 index 00000000..991d6330 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/exception/InventoryNotFoundException.java @@ -0,0 +1,40 @@ +package io.microprofile.tutorial.store.inventory.exception; + +import jakarta.ws.rs.core.Response; + +/** + * Exception thrown when an inventory item is not found. + */ +public class InventoryNotFoundException extends RuntimeException { + private Response.Status status; + + /** + * Constructs a new InventoryNotFoundException with the specified message. + * + * @param message the detail message + */ + public InventoryNotFoundException(String message) { + super(message); + this.status = Response.Status.NOT_FOUND; + } + + /** + * Constructs a new InventoryNotFoundException with the specified message and status. + * + * @param message the detail message + * @param status the HTTP status code to return + */ + public InventoryNotFoundException(String message, Response.Status status) { + super(message); + this.status = status; + } + + /** + * Gets the HTTP status associated with this exception. + * + * @return the HTTP status + */ + public Response.Status getStatus() { + return status; + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java new file mode 100644 index 00000000..c776c7ee --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/package-info.java @@ -0,0 +1,13 @@ +/** + * This package contains the Inventory Management application for the MicroProfile tutorial store. + * + * The application demonstrates a Jakarta EE and MicroProfile-based REST service + * for managing product inventory with CRUD operations. + * + * Main Components: + * - Entity class: Contains inventory data with inventory_id, product_id, and quantity + * - Repository: Provides in-memory data storage using HashMap + * - Service: Contains business logic and validation + * - Resource: REST endpoints with OpenAPI documentation + */ +package io.microprofile.tutorial.store.inventory; diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java new file mode 100644 index 00000000..05de8693 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/repository/InventoryRepository.java @@ -0,0 +1,168 @@ +package io.microprofile.tutorial.store.inventory.repository; + +import io.microprofile.tutorial.store.inventory.entity.Inventory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Thread-safe in-memory repository for Inventory objects. + * This class provides CRUD operations for Inventory entities to demonstrate MicroProfile concepts. + */ +@ApplicationScoped +public class InventoryRepository { + + private static final Logger LOGGER = Logger.getLogger(InventoryRepository.class.getName()); + + // Thread-safe map for inventory storage + private final Map inventories = new ConcurrentHashMap<>(); + + // Thread-safe ID generator + private final AtomicLong idGenerator = new AtomicLong(1); + + // Secondary index for faster lookups by productId + private final Map productToInventoryIndex = new ConcurrentHashMap<>(); + + /** + * Saves an inventory item to the repository. + * If the inventory has no ID, a new ID is assigned. + * + * @param inventory The inventory to save + * @return The saved inventory with ID assigned + */ + public Inventory save(Inventory inventory) { + // Generate ID if not provided + if (inventory.getInventoryId() == null) { + inventory.setInventoryId(idGenerator.getAndIncrement()); + } else { + // Update idGenerator if the provided ID is greater than current + long nextId = inventory.getInventoryId() + 1; + while (true) { + long currentId = idGenerator.get(); + if (nextId <= currentId || idGenerator.compareAndSet(currentId, nextId)) { + break; + } + } + } + + LOGGER.fine("Saving inventory with ID: " + inventory.getInventoryId()); + + // Update the inventory and secondary index + inventories.put(inventory.getInventoryId(), inventory); + productToInventoryIndex.put(inventory.getProductId(), inventory.getInventoryId()); + + return inventory; + } + + /** + * Finds an inventory item by ID. + * + * @param id The inventory ID + * @return An Optional containing the inventory if found, or empty if not found + */ + public Optional findById(Long id) { + if (id == null) { + LOGGER.warning("Attempted to find inventory with null ID"); + return Optional.empty(); + } + return Optional.ofNullable(inventories.get(id)); + } + + /** + * Finds inventory by product ID. + * + * @param productId The product ID + * @return An Optional containing the inventory if found, or empty if not found + */ + public Optional findByProductId(Long productId) { + if (productId == null) { + LOGGER.warning("Attempted to find inventory with null product ID"); + return Optional.empty(); + } + + // Use the secondary index for efficient lookup + Long inventoryId = productToInventoryIndex.get(productId); + if (inventoryId != null) { + return Optional.ofNullable(inventories.get(inventoryId)); + } + + // Fall back to scanning if not found in index (ensures consistency) + return inventories.values().stream() + .filter(inventory -> productId.equals(inventory.getProductId())) + .findFirst(); + } + + /** + * Retrieves all inventory items from the repository. + * + * @return A list of all inventory items + */ + public List findAll() { + return new ArrayList<>(inventories.values()); + } + + /** + * Deletes an inventory item by ID. + * + * @param id The ID of the inventory to delete + * @return true if the inventory was deleted, false if not found + */ + public boolean deleteById(Long id) { + if (id == null) { + LOGGER.warning("Attempted to delete inventory with null ID"); + return false; + } + + Inventory removed = inventories.remove(id); + if (removed != null) { + // Also remove from the secondary index + productToInventoryIndex.remove(removed.getProductId()); + LOGGER.fine("Deleted inventory with ID: " + id); + return true; + } + + LOGGER.fine("Failed to delete inventory with ID (not found): " + id); + return false; + } + + /** + * Updates an existing inventory item. + * + * @param id The ID of the inventory to update + * @param inventory The updated inventory information + * @return An Optional containing the updated inventory, or empty if not found + */ + public Optional update(Long id, Inventory inventory) { + if (id == null || inventory == null) { + LOGGER.warning("Attempted to update inventory with null ID or null inventory"); + return Optional.empty(); + } + + if (!inventories.containsKey(id)) { + LOGGER.fine("Failed to update inventory with ID (not found): " + id); + return Optional.empty(); + } + + // Get the existing inventory to update its product index if needed + Inventory existing = inventories.get(id); + if (existing != null && !existing.getProductId().equals(inventory.getProductId())) { + // Product ID changed, update the index + productToInventoryIndex.remove(existing.getProductId()); + } + + // Set ID and update the repository + inventory.setInventoryId(id); + inventories.put(id, inventory); + productToInventoryIndex.put(inventory.getProductId(), id); + + LOGGER.fine("Updated inventory with ID: " + id); + return Optional.of(inventory); + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java new file mode 100644 index 00000000..32189b02 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java @@ -0,0 +1,266 @@ +package io.microprofile.tutorial.store.inventory.resource; + +import io.microprofile.tutorial.store.inventory.entity.Inventory; +import io.microprofile.tutorial.store.inventory.service.InventoryService; +import io.microprofile.tutorial.store.inventory.dto.Product; + +import java.net.URI; +import java.util.List; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * REST resource for inventory operations. + */ +@Path("/inventories") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Inventory", description = "Operations related to product inventory management") +public class InventoryResource { + + @Inject + private InventoryService inventoryService; + + @Context + private UriInfo uriInfo; + + @GET + @Operation(summary = "Get all inventory items", description = "Returns a paginated list of inventory items with optional filtering") + @APIResponse( + responseCode = "200", + description = "List of inventory items", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Inventory.class) + ) + ) + public Response getAllInventories( + @Parameter(description = "Page number (zero-based)", schema = @Schema(defaultValue = "0")) + @QueryParam("page") @DefaultValue("0") int page, + + @Parameter(description = "Page size", schema = @Schema(defaultValue = "20")) + @QueryParam("size") @DefaultValue("20") int size, + + @Parameter(description = "Filter by minimum quantity") + @QueryParam("minQuantity") Integer minQuantity, + + @Parameter(description = "Filter by maximum quantity") + @QueryParam("maxQuantity") Integer maxQuantity) { + + List inventories = inventoryService.getAllInventories(page, size, minQuantity, maxQuantity); + long totalCount = inventoryService.countInventories(minQuantity, maxQuantity); + + return Response.ok(inventories) + .header("X-Total-Count", totalCount) + .header("X-Page-Number", page) + .header("X-Page-Size", size) + .build(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Get inventory item by ID", description = "Returns a specific inventory item by ID") + @APIResponse( + responseCode = "200", + description = "Inventory item", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found" + ) + public Inventory getInventoryById( + @Parameter(description = "ID of the inventory item", required = true) + @PathParam("id") Long id) { + return inventoryService.getInventoryById(id); + } + + @GET + @Path("/product/{productId}") + @Operation(summary = "Get inventory item by product ID", description = "Returns inventory information for a specific product") + @APIResponse( + responseCode = "200", + description = "Inventory item", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found for product" + ) + public Inventory getInventoryByProductId( + @Parameter(description = "Product ID", required = true) + @PathParam("productId") Long productId) { + return inventoryService.getInventoryByProductId(productId); + } + + @POST + @Operation(summary = "Create new inventory item", description = "Creates a new inventory item") + @APIResponse( + responseCode = "201", + description = "Inventory created", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "409", + description = "Inventory for product already exists" + ) + public Response createInventory( + @Parameter(description = "Inventory details", required = true) + @NotNull @Valid Inventory inventory) { + Inventory createdInventory = inventoryService.createInventory(inventory); + URI location = uriInfo.getAbsolutePathBuilder().path(createdInventory.getInventoryId().toString()).build(); + return Response.created(location).entity(createdInventory).build(); + } + + @PUT + @Path("/{id}") + @Operation(summary = "Update inventory item", description = "Updates an existing inventory item") + @APIResponse( + responseCode = "200", + description = "Inventory updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found" + ) + @APIResponse( + responseCode = "409", + description = "Another inventory record already exists for this product" + ) + public Inventory updateInventory( + @Parameter(description = "ID of the inventory item", required = true) + @PathParam("id") Long id, + @Parameter(description = "Updated inventory details", required = true) + @NotNull @Valid Inventory inventory) { + return inventoryService.updateInventory(id, inventory); + } + + @DELETE + @Path("/{id}") + @Operation(summary = "Delete inventory item", description = "Deletes an inventory item") + @APIResponse( + responseCode = "204", + description = "Inventory deleted" + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found" + ) + public Response deleteInventory( + @Parameter(description = "ID of the inventory item", required = true) + @PathParam("id") Long id) { + inventoryService.deleteInventory(id); + return Response.noContent().build(); + } + + @PATCH + @Path("/product/{productId}/quantity/{quantity}") + @Operation(summary = "Update product quantity", description = "Updates the quantity for a specific product") + @APIResponse( + responseCode = "200", + description = "Quantity updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found for product" + ) + public Inventory updateQuantity( + @Parameter(description = "Product ID", required = true) + @PathParam("productId") Long productId, + @Parameter(description = "New quantity", required = true) + @PathParam("quantity") int quantity) { + return inventoryService.updateQuantity(productId, quantity); + } + + @PATCH + @Path("/product/{productId}/reserve/{quantity}") + @Operation(summary = "Reserve inventory for a product", + description = "Reserves the specified quantity of inventory for a product if it's available in the catalog") + @APIResponse( + responseCode = "200", + description = "Inventory reserved successfully", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "400", + description = "Invalid quantity or insufficient inventory available" + ) + @APIResponse( + responseCode = "404", + description = "Product not found in catalog or inventory not found" + ) + public Inventory reserveInventory( + @Parameter(description = "Product ID", required = true) + @PathParam("productId") Long productId, + @Parameter(description = "Quantity to reserve", required = true) + @PathParam("quantity") int quantity) { + return inventoryService.reserveInventory(productId, quantity); + } + + @GET + @Path("/product-info/{productId}") + @Operation(summary = "Get product information using custom RestClientBuilder", + description = "Demonstrates advanced RestClientBuilder usage with custom timeout configuration") + @APIResponse( + responseCode = "200", + description = "Product information retrieved successfully", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Product.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Product not found" + ) + public Response getProductInfo( + @Parameter(description = "Product ID", required = true) + @PathParam("productId") Long productId) { + + Product product = inventoryService.getProductWithCustomClient(productId); + if (product != null) { + return Response.ok(product).build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"message\": \"Product not found\"}") + .build(); + } + } +} diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java new file mode 100644 index 00000000..3ee718c0 --- /dev/null +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java @@ -0,0 +1,493 @@ +package io.microprofile.tutorial.store.inventory.service; + +import io.microprofile.tutorial.store.inventory.entity.Inventory; +import io.microprofile.tutorial.store.inventory.exception.InventoryConflictException; +import io.microprofile.tutorial.store.inventory.exception.InventoryNotFoundException; +import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; +import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; +import io.microprofile.tutorial.store.inventory.dto.Product; +import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.WebApplicationException; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +/** + * Service class for Inventory management operations. + */ +@ApplicationScoped +public class InventoryService { + + private static final Logger LOGGER = Logger.getLogger(InventoryService.class.getName()); + + @Inject + private InventoryRepository inventoryRepository; + + @Inject + @RestClient + private ProductServiceClient productServiceClient; + + /** + * Checks if a product is available in the catalog service. + * This method demonstrates the use of RestClientBuilder for programmatic REST client creation. + * This is a lightweight check that returns only a boolean result. + * + * @param productId The product ID to check + * @return true if the product exists, false otherwise + */ + public boolean isProductAvailable(Long productId) { + LOGGER.fine("Checking product availability for ID: " + productId); + + try { + // Demonstrate RestClientBuilder usage with MP Rest Client 4.0 + // Using new baseUri(String) method - no need for URI.create() + ProductServiceClient dynamicClient = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(ProductServiceClient.class); + + LOGGER.fine("Built dynamic REST client for catalog service using baseUri(String)"); + + Product product = dynamicClient.getProductById(productId); + boolean available = product != null; + LOGGER.fine("Product " + productId + " availability check via RestClientBuilder: " + available); + return available; + + } catch (WebApplicationException e) { + if (e.getResponse().getStatus() == 404) { + LOGGER.fine("Product " + productId + " not found in catalog (via RestClientBuilder)"); + return false; + } + LOGGER.warning("Error checking product availability for ID " + productId + " via RestClientBuilder: " + e.getMessage()); + return false; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unexpected error checking product availability for ID " + productId + " via RestClientBuilder", e); + return false; + } + } + + /** + * Validates that a product exists in the catalog service. + * + * @param productId The product ID to validate + * @return The product details if found + * @throws InventoryNotFoundException if the product is not found in the catalog + */ + private Product validateProductExists(Long productId) { + LOGGER.fine("Validating product existence for ID: " + productId); + + try { + Product product = productServiceClient.getProductById(productId); + if (product == null) { + throw new InventoryNotFoundException("Product not found in catalog with ID: " + productId); + } + LOGGER.fine("Product validated successfully: " + product.getName()); + return product; + } catch (InventoryNotFoundException e) { + // Re-throw InventoryNotFoundException without wrapping + throw e; + } catch (WebApplicationException e) { + LOGGER.warning("Product validation failed for ID " + productId + ": " + e.getMessage()); + if (e.getResponse().getStatus() == 404) { + throw new InventoryNotFoundException("Product not found in catalog with ID: " + productId); + } + throw new RuntimeException("Failed to validate product with catalog service: " + e.getMessage(), e); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unexpected error validating product " + productId, e); + throw new RuntimeException("Failed to validate product with catalog service: " + e.getMessage(), e); + } + } + + /** + * Creates a new inventory item. + * + * @param inventory The inventory to create + * @return The created inventory + * @throws InventoryConflictException if inventory with the product ID already exists + */ + @Transactional + public Inventory createInventory(Inventory inventory) { + LOGGER.info("Creating inventory for product ID: " + inventory.getProductId()); + + // Validate that the product exists in the catalog service + Product product = validateProductExists(inventory.getProductId()); + LOGGER.info("Product validated: " + product.getName() + " (Price: $" + product.getPrice() + ")"); + + // Check if product ID already exists + Optional existingInventory = inventoryRepository.findByProductId(inventory.getProductId()); + if (existingInventory.isPresent()) { + LOGGER.warning("Conflict: Inventory already exists for product ID: " + inventory.getProductId()); + throw new InventoryConflictException("Inventory for product already exists", Response.Status.CONFLICT); + } + + Inventory result = inventoryRepository.save(inventory); + LOGGER.info("Created inventory ID: " + result.getInventoryId() + " for product ID: " + result.getProductId()); + return result; + } + + /** + * Creates new inventory items in bulk. + * + * @param inventories The list of inventories to create + * @return The list of created inventories + * @throws InventoryConflictException if any inventory with the same product ID already exists + */ + @Transactional + public List createBulkInventories(List inventories) { + LOGGER.info("Creating bulk inventories: " + inventories.size() + " items"); + + // Validate products exist in catalog and check for conflicts + for (Inventory inventory : inventories) { + // Validate product exists in catalog + Product product = validateProductExists(inventory.getProductId()); + LOGGER.fine("Product validated for bulk create: " + product.getName() + " (ID: " + inventory.getProductId() + ")"); + + // Check for existing inventory records + Optional existingInventory = inventoryRepository.findByProductId(inventory.getProductId()); + if (existingInventory.isPresent()) { + LOGGER.warning("Conflict detected during bulk create for product ID: " + inventory.getProductId()); + throw new InventoryConflictException("Inventory for product already exists: " + inventory.getProductId()); + } + } + + // Save all inventories + List created = new ArrayList<>(); + for (Inventory inventory : inventories) { + created.add(inventoryRepository.save(inventory)); + } + + LOGGER.info("Successfully created " + created.size() + " inventory items"); + return created; + } + + /** + * Gets an inventory item by ID. + * + * @param id The inventory ID + * @return The inventory + * @throws InventoryNotFoundException if the inventory is not found + */ + public Inventory getInventoryById(Long id) { + LOGGER.fine("Getting inventory by ID: " + id); + return inventoryRepository.findById(id) + .orElseThrow(() -> { + LOGGER.warning("Inventory not found with ID: " + id); + return new InventoryNotFoundException("Inventory not found with ID: " + id); + }); + } + + /** + * Gets inventory by product ID. + * + * @param productId The product ID + * @return The inventory + * @throws InventoryNotFoundException if the inventory is not found + */ + public Inventory getInventoryByProductId(Long productId) { + LOGGER.fine("Getting inventory by product ID: " + productId); + return inventoryRepository.findByProductId(productId) + .orElseThrow(() -> { + LOGGER.warning("Inventory not found for product ID: " + productId); + return new InventoryNotFoundException("Inventory not found for product", Response.Status.NOT_FOUND); + }); + } + + /** + * Gets all inventory items. + * + * @return A list of all inventory items + */ + public List getAllInventories() { + LOGGER.fine("Getting all inventory items"); + return inventoryRepository.findAll(); + } + + /** + * Gets inventory items with pagination and filtering. + * + * @param page Page number (zero-based) + * @param size Page size + * @param minQuantity Minimum quantity filter (optional) + * @param maxQuantity Maximum quantity filter (optional) + * @return A filtered and paginated list of inventory items + */ + public List getAllInventories(int page, int size, Integer minQuantity, Integer maxQuantity) { + LOGGER.fine("Getting inventory items with pagination: page=" + page + ", size=" + size + + ", minQuantity=" + minQuantity + ", maxQuantity=" + maxQuantity); + + // First, get all inventories + List allInventories = inventoryRepository.findAll(); + + // Apply filters if provided + List filteredInventories = allInventories.stream() + .filter(inv -> minQuantity == null || inv.getQuantity() >= minQuantity) + .filter(inv -> maxQuantity == null || inv.getQuantity() <= maxQuantity) + .collect(Collectors.toList()); + + // Apply pagination + int startIndex = page * size; + int endIndex = Math.min(startIndex + size, filteredInventories.size()); + + // Check if the start index is valid + if (startIndex >= filteredInventories.size()) { + return new ArrayList<>(); + } + + return filteredInventories.subList(startIndex, endIndex); + } + + /** + * Counts inventory items with filtering. + * + * @param minQuantity Minimum quantity filter (optional) + * @param maxQuantity Maximum quantity filter (optional) + * @return The count of inventory items that match the filters + */ + public long countInventories(Integer minQuantity, Integer maxQuantity) { + LOGGER.fine("Counting inventory items with filters: minQuantity=" + minQuantity + + ", maxQuantity=" + maxQuantity); + + List allInventories = inventoryRepository.findAll(); + + // Apply filters and count + return allInventories.stream() + .filter(inv -> minQuantity == null || inv.getQuantity() >= minQuantity) + .filter(inv -> maxQuantity == null || inv.getQuantity() <= maxQuantity) + .count(); + } + + /** + * Updates an inventory item. + * + * @param id The inventory ID + * @param inventory The updated inventory information + * @return The updated inventory + * @throws InventoryNotFoundException if the inventory is not found + * @throws InventoryConflictException if another inventory with the same product ID exists + */ + @Transactional + public Inventory updateInventory(Long id, Inventory inventory) { + LOGGER.info("Updating inventory ID: " + id + " for product ID: " + inventory.getProductId()); + + // Validate that the product exists in the catalog service + Product product = validateProductExists(inventory.getProductId()); + LOGGER.info("Product validated for update: " + product.getName() + " (ID: " + product.getId() + ")"); + + // Check if product ID exists in a different inventory record + Optional existingInventoryWithProductId = inventoryRepository.findByProductId(inventory.getProductId()); + if (existingInventoryWithProductId.isPresent() && + !existingInventoryWithProductId.get().getInventoryId().equals(id)) { + LOGGER.warning("Conflict: Another inventory record exists for product ID: " + inventory.getProductId()); + throw new InventoryConflictException("Another inventory record already exists for this product", + Response.Status.CONFLICT); + } + + return inventoryRepository.update(id, inventory) + .orElseThrow(() -> { + LOGGER.warning("Inventory not found with ID: " + id); + return new InventoryNotFoundException("Inventory not found", Response.Status.NOT_FOUND); + }); + } + + /** + * Deletes an inventory item. + * + * @param id The inventory ID + * @throws InventoryNotFoundException if the inventory is not found + */ + @Transactional + public void deleteInventory(Long id) { + LOGGER.info("Deleting inventory with ID: " + id); + boolean deleted = inventoryRepository.deleteById(id); + if (!deleted) { + LOGGER.warning("Inventory not found with ID: " + id); + throw new InventoryNotFoundException("Inventory not found", Response.Status.NOT_FOUND); + } + LOGGER.info("Successfully deleted inventory with ID: " + id); + } + + /** + * Updates the quantity for a product. + * + * @param productId The product ID + * @param quantity The new quantity + * @return The updated inventory + * @throws InventoryNotFoundException if the inventory is not found + */ + @Transactional + public Inventory updateQuantity(Long productId, int quantity) { + if (quantity < 0) { + LOGGER.warning("Invalid quantity: " + quantity + " for product ID: " + productId); + throw new IllegalArgumentException("Quantity cannot be negative"); + } + + LOGGER.info("Updating quantity to " + quantity + " for product ID: " + productId); + Inventory inventory = getInventoryByProductId(productId); + int oldQuantity = inventory.getQuantity(); + inventory.setQuantity(quantity); + + Inventory updated = inventoryRepository.save(inventory); + LOGGER.info("Updated quantity from " + oldQuantity + " to " + quantity + + " for product ID: " + productId + " (inventory ID: " + inventory.getInventoryId() + ")"); + + return updated; + } + + /** + * Gets product information for an inventory item. + * + * @param inventory The inventory item + * @return The product details + */ + public Product getProductInfo(Inventory inventory) { + return validateProductExists(inventory.getProductId()); + } + + /** + * Gets inventory with enriched product information. + * + * @param inventoryId The inventory ID + * @return Inventory with product details + */ + public InventoryWithProductInfo getInventoryWithProductInfo(Long inventoryId) { + Inventory inventory = getInventoryById(inventoryId); + Product product = validateProductExists(inventory.getProductId()); + + return new InventoryWithProductInfo(inventory, product); + } + + /** + * Gets all inventories with product information for a specific category. + * + * @param category The product category + * @return List of inventories for products in the specified category + */ + public List getInventoriesByCategory(String category) { + LOGGER.info("Getting inventories for category: " + category); + + try { + // Get products by category from catalog service + List productsInCategory = productServiceClient.getProductsByCategory(category); + + if (productsInCategory == null || productsInCategory.isEmpty()) { + LOGGER.info("No products found in category: " + category); + return new ArrayList<>(); + } + + // Find inventories for these products + List result = new ArrayList<>(); + for (Product product : productsInCategory) { + try { + Inventory inventory = inventoryRepository.findByProductId(product.getId()).orElse(null); + if (inventory != null) { + result.add(new InventoryWithProductInfo(inventory, product)); + } + } catch (Exception e) { + LOGGER.warning("Error getting inventory for product " + product.getId() + ": " + e.getMessage()); + } + } + + LOGGER.info("Found " + result.size() + " inventory items for category: " + category); + return result; + + } catch (WebApplicationException e) { + LOGGER.warning("Failed to get products by category from catalog service: " + e.getMessage()); + throw new RuntimeException("Failed to retrieve products by category: " + e.getMessage(), e); + } + } + + /** + * Reserves inventory for a product if it's available in the catalog. + * This method uses isProductAvailable for a lightweight check before reservation. + * + * @param productId The product ID + * @param quantityToReserve The quantity to reserve + * @return The updated inventory after reservation + * @throws InventoryNotFoundException if the inventory or product is not found + * @throws IllegalArgumentException if there's insufficient inventory + */ + @Transactional + public Inventory reserveInventory(Long productId, int quantityToReserve) { + if (quantityToReserve <= 0) { + throw new IllegalArgumentException("Quantity to reserve must be positive"); + } + + LOGGER.info("Attempting to reserve " + quantityToReserve + " units for product ID: " + productId); + + // Use isProductAvailable for a lightweight availability check + if (!isProductAvailable(productId)) { + LOGGER.warning("Cannot reserve inventory - product " + productId + " is not available in catalog"); + throw new InventoryNotFoundException("Product is not available in catalog: " + productId); + } + + // Get the current inventory + Inventory inventory = getInventoryByProductId(productId); + + // Check if we have enough inventory to reserve + int availableQuantity = inventory.getQuantity() - inventory.getReservedQuantity(); + if (availableQuantity < quantityToReserve) { + LOGGER.warning("Insufficient inventory to reserve " + quantityToReserve + + " units for product " + productId + ". Available: " + availableQuantity); + throw new IllegalArgumentException("Insufficient inventory available. Requested: " + + quantityToReserve + ", Available: " + availableQuantity); + } + + // Update reserved quantity + inventory.setReservedQuantity(inventory.getReservedQuantity() + quantityToReserve); + + Inventory updated = inventoryRepository.save(inventory); + LOGGER.info("Reserved " + quantityToReserve + " units for product " + productId + + ". New reserved quantity: " + updated.getReservedQuantity()); + + return updated; + } + + /** + * Demonstrates advanced RestClientBuilder usage with custom configuration. + * This method builds a REST client with specific timeout and error handling settings. + * Uses MicroProfile Rest Client 4.0 baseUri(String) convenience method. + * + * @param productId The product ID to check + * @return Product details if found, null otherwise + */ + public Product getProductWithCustomClient(Long productId) { + LOGGER.info("Getting product details using custom RestClientBuilder for ID: " + productId); + + try { + // Build REST client with custom configuration using MP Rest Client 4.0 + // Using baseUri(String) - no URI.create() needed + ProductServiceClient customClient = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(3, TimeUnit.SECONDS) // Custom connect timeout + .readTimeout(8, TimeUnit.SECONDS) // Custom read timeout + .build(ProductServiceClient.class); + + LOGGER.info("Built custom REST client with 3s connect and 8s read timeout"); + + Product product = customClient.getProductById(productId); + LOGGER.info("Retrieved product via custom client: " + (product != null ? product.getName() : "null")); + return product; + + } catch (WebApplicationException e) { + LOGGER.warning("WebApplicationException from custom client for product " + productId + + ": Status=" + e.getResponse().getStatus() + ", Message=" + e.getMessage()); + return null; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unexpected error from custom REST client for product " + productId, e); + return null; + } + } +} diff --git a/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties b/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties new file mode 100644 index 00000000..26358a61 --- /dev/null +++ b/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties @@ -0,0 +1,5 @@ +product-service/mp-rest/url=http://localhost:5050/catalog/api +product-service/mp-rest/scope=jakarta.inject.Singleton +product-service/mp-rest/connectTimeout=5000 +product-service/mp-rest/readTimeout=10000 +product-service/mp-rest/followRedirects=true \ No newline at end of file diff --git a/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml b/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 00000000..ba21c474 --- /dev/null +++ b/code/chapter11/inventory/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,7 @@ + + + diff --git a/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml b/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..5a812df9 --- /dev/null +++ b/code/chapter11/inventory/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + Inventory Management + + index.html + + diff --git a/code/chapter11/inventory/src/main/webapp/index.html b/code/chapter11/inventory/src/main/webapp/index.html new file mode 100644 index 00000000..d4763767 --- /dev/null +++ b/code/chapter11/inventory/src/main/webapp/index.html @@ -0,0 +1,350 @@ + + + + + + Inventory Management Service + + + +

Inventory Management Service

+

Welcome to the Inventory Management API, a Jakarta EE and MicroProfile demo featuring comprehensive MicroProfile Rest Client integration.

+ +
+

🔌 MicroProfile Rest Client Integration

+

This service demonstrates three different approaches to using MicroProfile Rest Client:

+
    +
  • CDI Injection (@RestClient) - For standard product validation
  • +
  • RestClientBuilder (5s/10s timeout) - For lightweight availability checks
  • +
  • Advanced RestClientBuilder (3s/8s timeout) - For detailed product information
  • +
+

Catalog Service Integration: http://localhost:5050/catalog/api

+
+ +
+

⏱️ Timeout Configuration Details

+

Our implementation demonstrates different timeout strategies for various use cases:

+ +
+
+

🔌 CDI Injection (@RestClient)

+

Configuration: Via microprofile-config.properties

+

Connect Timeout: Default (30s)

+

Read Timeout: Default (30s)

+

Use Case: Standard operations with reliable timeouts

+
+ +
+

⚡ RestClientBuilder (5s/10s)

+

Configuration: Programmatic

+

Connect Timeout: 5 seconds

+

Read Timeout: 10 seconds

+

Use Case: Quick availability checks

+
+ +
+

🚀 Advanced RestClientBuilder (3s/8s)

+

Configuration: Programmatic

+

Connect Timeout: 3 seconds

+

Read Timeout: 8 seconds

+

Use Case: Fast product info retrieval

+
+
+ +

📊 Timeout Configuration Comparison

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Client TypeConnect TimeoutRead TimeoutConfiguration MethodEndpoints Using ItPurpose
@RestClient Injection30s (default)30s (default)microprofile-config.propertiesPOST/PUT inventories, bulk operationsReliable product validation
RestClientBuilder (Standard)5 seconds10 secondsRestClientBuilder.connectTimeout()PATCH /reserve/{quantity}Quick availability checks
RestClientBuilder (Advanced)3 seconds8 secondsRestClientBuilder.readTimeout()GET /product-info/{productId}Fast product information
+ +

🔧 Timeout Configuration Code Examples

+

1. CDI Injection Configuration (microprofile-config.properties):

+
# Default timeouts - can be customized via properties
+io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/url=http://localhost:5050/catalog/api
+io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/scope=javax.inject.Singleton
+
+# Optional custom timeouts (if needed):
+# io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/connectTimeout=30000
+# io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/readTimeout=30000
+ +

2. RestClientBuilder with 5s/10s Timeouts (Availability Check):

+
ProductServiceClient dynamicClient = RestClientBuilder.newBuilder()
+    .baseUri(URI.create("http://localhost:5050/catalog/api"))
+    .connectTimeout(5, TimeUnit.SECONDS)    // 5 seconds to establish connection
+    .readTimeout(10, TimeUnit.SECONDS)      // 10 seconds to read response
+    .build(ProductServiceClient.class);
+ +

3. Advanced RestClientBuilder with 3s/8s Timeouts (Product Info):

+
ProductServiceClient customClient = RestClientBuilder.newBuilder()
+    .baseUri(URI.create("http://localhost:5050/catalog/api"))
+    .connectTimeout(3, TimeUnit.SECONDS)    // 3 seconds to establish connection
+    .readTimeout(8, TimeUnit.SECONDS)       // 8 seconds to read response
+    .build(ProductServiceClient.class);
+ +

📈 Timeout Strategy Benefits

+
    +
  • Connect Timeout: Prevents hanging when catalog service is unreachable
  • +
  • Read Timeout: Ensures timely response even if catalog service is slow
  • +
  • Different Strategies: Optimized timeouts for different operation types
  • +
  • Fail-Fast Behavior: Quick error detection and graceful degradation
  • +
  • Performance Optimization: Shorter timeouts for non-critical operations
  • +
+
+ +

Available Endpoints:

+ +
+

OpenAPI Documentation API Docs

+

GET /openapi - Access OpenAPI documentation

+ View API Documentation +
+ +
+

Basic Inventory Operations

+

GET /api/inventories - Get all inventory items

+

GET /api/inventories/{id} - Get inventory by ID

+

GET /api/inventories/product/{productId} - Get inventory by product ID

+

POST /api/inventories - Create new inventory @RestClient Validation

+

PUT /api/inventories/{id} - Update inventory @RestClient Validation

+

DELETE /api/inventories/{id} - Delete inventory

+

PATCH /api/inventories/product/{productId}/quantity/{quantity} - Update product quantity

+
+ +
+

🔌 MicroProfile Rest Client Features

+ +

1. Injected REST Client (@RestClient)

+

POST /api/inventories - Product validation during inventory creation

+

PUT /api/inventories/{id} - Product validation during inventory updates

+

POST /api/inventories/bulk - Bulk inventory creation with validation

+

GET /api/inventories/{id}/with-product-info - Enriched inventory with product details

+

GET /api/inventories/category/{category} - Inventories filtered by product category

+ +

2. RestClientBuilder (5s connect / 10s read timeout)

+

PATCH /api/inventories/product/{productId}/reserve/{quantity} - Reserve inventory with availability check

+ +

3. Advanced RestClientBuilder (3s connect / 8s read timeout)

+

GET /api/inventories/product-info/{productId} - Get detailed product information

+
+ +
+

🚀 Advanced Features

+

GET /api/inventories?page={page}&size={size} - Pagination support

+

GET /api/inventories?minQuantity={min}&maxQuantity={max} - Quantity filtering

+

GET /api/inventories/count?minQuantity={min}&maxQuantity={max} - Count with filters

+

POST /api/inventories/bulk - Bulk inventory operations

+
+ +

Example Requests

+ +
+

💡 Quick Start Examples

+ +

1. Basic Operations

+
# Get all inventories
+curl -X GET http://localhost:7050/inventory/api/inventories
+
+# Create inventory (with automatic product validation)
+curl -X POST http://localhost:7050/inventory/api/inventories \
+  -H "Content-Type: application/json" \
+  -d '{"productId": 1, "quantity": 100, "reservedQuantity": 0}'
+ +

2. MicroProfile Rest Client Features

+
# Reserve inventory (uses RestClientBuilder for availability check)
+curl -X PATCH http://localhost:7050/inventory/api/inventories/product/1/reserve/10
+
+# Get product info (uses Advanced RestClientBuilder)
+curl -X GET http://localhost:7050/inventory/api/inventories/product-info/1
+
+# Get enriched inventory with product details
+curl -X GET http://localhost:7050/inventory/api/inventories/1/with-product-info
+ +

3. Advanced Features

+
# Pagination and filtering
+curl -X GET "http://localhost:7050/inventory/api/inventories?page=0&size=5&minQuantity=50"
+
+# Bulk operations
+curl -X POST http://localhost:7050/inventory/api/inventories/bulk \
+  -H "Content-Type: application/json" \
+  -d '[{"productId": 1, "quantity": 100}, {"productId": 2, "quantity": 50}]'
+
+ +
+

⚙️ Configuration & Testing

+

Test Scripts Available:

+
    +
  • ./test-inventory-endpoints.sh - Comprehensive test suite
  • +
  • ./test-inventory-endpoints.sh --restclient - RestClient features only
  • +
  • ./quick-test-commands.sh - Command reference
  • +
+ +

Service Dependencies:

+
    +
  • Catalog Service: http://localhost:5050
  • +
  • Inventory Service: http://localhost:7050
  • +
+ +

MicroProfile Config:

+
io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/url=http://localhost:5050/catalog/api
+io.microprofile.tutorial.store.inventory.client.ProductServiceClient/mp-rest/scope=javax.inject.Singleton
+
+ +

🏗️ Architecture

+
+

MicroProfile Rest Client Integration Patterns

+

Pattern 1 - CDI Injection: Automatic client injection with configuration-driven setup

+

Pattern 2 - Programmatic Creation: Dynamic client building with custom timeouts and error handling

+

Pattern 3 - Advanced Configuration: Per-use-case client optimization

+ +

Technologies Used:

+
    +
  • Jakarta EE 10
  • +
  • MicroProfile 6.1 (Rest Client, OpenAPI, Config)
  • +
  • Open Liberty 24.0.0.x
  • +
  • Jackson for JSON processing
  • +
  • Lombok for reduced boilerplate
  • +
+
+ +
+

MicroProfile REST Client Tutorial - Inventory Service

+

Demonstrates comprehensive MicroProfile Rest Client integration with Jakarta EE

+

© 2025 - Updated June 7, 2025

+
+ + diff --git a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java new file mode 100644 index 00000000..a4349eb7 --- /dev/null +++ b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/integration/InventoryServiceIntegrationTest.java @@ -0,0 +1,154 @@ +package io.microprofile.tutorial.store.inventory.integration; + +import io.microprofile.tutorial.store.inventory.service.InventoryService; +import io.microprofile.tutorial.store.inventory.entity.Inventory; +import io.microprofile.tutorial.store.inventory.dto.Product; +import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; +import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; + +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 java.lang.reflect.Field; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Integration tests for InventoryService with ProductServiceClient. + * This test class focuses on the main integration points. + */ +@ExtendWith(MockitoExtension.class) +class InventoryServiceIntegrationTest { + + @Mock + private InventoryRepository inventoryRepository; + + @Mock + private ProductServiceClient productServiceClient; + + @InjectMocks + private InventoryService inventoryService; + + private Product mockProduct; + private Inventory mockInventory; + + @BeforeEach + void setUp() throws Exception { + // Create mock product using constructor + mockProduct = new Product(1L, "Test Product", 29.99, "Electronics", "A test product"); + + // Create mock inventory with proper productId set using reflection + mockInventory = new Inventory(); + setPrivateField(mockInventory, "inventoryId", 1L); + setPrivateField(mockInventory, "productId", 1L); + setPrivateField(mockInventory, "quantity", 10); + setPrivateField(mockInventory, "reservedQuantity", 2); + } + + private void setPrivateField(Object obj, String fieldName, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } + + private Object getPrivateField(Object obj, String fieldName) throws Exception { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } + + @Test + void testProductServiceClientIntegration_BasicCall() throws Exception { + // Arrange + lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); + + // Act + Product result = inventoryService.getProductInfo(mockInventory); + + // Assert + assertNotNull(result); + assertEquals(1L, getPrivateField(result, "id")); + verify(productServiceClient).getProductById(1L); + } + + @Test + void testCreateInventory_CallsProductValidation() throws Exception { + // Arrange + Inventory newInventory = new Inventory(); + setPrivateField(newInventory, "productId", 1L); + setPrivateField(newInventory, "quantity", 5); + setPrivateField(newInventory, "reservedQuantity", 0); + + lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); + lenient().when(inventoryRepository.findByProductId(anyLong())).thenReturn(Optional.empty()); + lenient().when(inventoryRepository.save(any(Inventory.class))).thenReturn(mockInventory); + + // Act + Inventory result = inventoryService.createInventory(newInventory); + + // Assert + assertNotNull(result); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).save(newInventory); + } + + @Test + void testUpdateInventory_CallsProductValidation() throws Exception { + // Arrange + Inventory updatedInventory = new Inventory(); + setPrivateField(updatedInventory, "productId", 1L); + setPrivateField(updatedInventory, "quantity", 15); + + lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); + lenient().when(inventoryRepository.findByProductId(anyLong())).thenReturn(Optional.of(mockInventory)); + lenient().when(inventoryRepository.update(anyLong(), any(Inventory.class))).thenReturn(Optional.of(mockInventory)); + + // Act + Inventory result = inventoryService.updateInventory(1L, updatedInventory); + + // Assert + assertNotNull(result); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).update(1L, updatedInventory); + } + + @Test + void testProductServiceClient_ReturnsProductData() throws Exception { + // Arrange + lenient().when(productServiceClient.getProductById(anyLong())).thenReturn(mockProduct); + + // Act + Product result = inventoryService.getProductInfo(mockInventory); + + // Assert + assertNotNull(result); + // Test using reflection to access private fields + assertEquals("Test Product", getPrivateField(result, "name")); + assertEquals("Electronics", getPrivateField(result, "category")); + verify(productServiceClient).getProductById(1L); + } + + @Test + void testCreateInventory_WithInvalidProduct_ThrowsException() throws Exception { + // Arrange + Inventory newInventory = new Inventory(); + setPrivateField(newInventory, "productId", 999L); + setPrivateField(newInventory, "quantity", 5); + + lenient().when(productServiceClient.getProductById(999L)).thenReturn(null); + + // Act & Assert + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + inventoryService.createInventory(newInventory); + }); + + assertTrue(exception.getMessage().contains("Product not found in catalog with ID: 999")); + verify(productServiceClient).getProductById(999L); + } +} \ No newline at end of file diff --git a/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java new file mode 100644 index 00000000..a7859a74 --- /dev/null +++ b/code/chapter11/inventory/src/test/java/io/microprofile/tutorial/store/inventory/service/InventoryServiceTest.java @@ -0,0 +1,302 @@ +package io.microprofile.tutorial.store.inventory.service; + +import io.microprofile.tutorial.store.inventory.entity.Inventory; +import io.microprofile.tutorial.store.inventory.exception.InventoryNotFoundException; +import io.microprofile.tutorial.store.inventory.exception.InventoryConflictException; +import io.microprofile.tutorial.store.inventory.repository.InventoryRepository; +import io.microprofile.tutorial.store.inventory.client.ProductServiceClient; +import io.microprofile.tutorial.store.inventory.dto.Product; +import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; + +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 jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for InventoryService with ProductServiceClient integration. + */ +@ExtendWith(MockitoExtension.class) +class InventoryServiceTest { + + @Mock + private InventoryRepository inventoryRepository; + + @Mock + private ProductServiceClient productServiceClient; + + @InjectMocks + private InventoryService inventoryService; + + private Product mockProduct; + private Inventory mockInventory; + + @BeforeEach + void setUp() { + mockProduct = new Product(1L, "Test Product", 29.99, "Electronics", "A test product"); + + mockInventory = new Inventory(); + mockInventory.setInventoryId(1L); + mockInventory.setProductId(1L); + mockInventory.setQuantity(100); + mockInventory.setReservedQuantity(10); + } + + @Test + void testCreateInventory_WithValidProduct_ShouldSucceed() { + // Arrange + Inventory newInventory = Inventory.builder() + .productId(1L) + .quantity(50) + .reservedQuantity(0) + .build(); + + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.empty()); + when(inventoryRepository.save(any(Inventory.class))).thenReturn(mockInventory); + + // Act + Inventory result = inventoryService.createInventory(newInventory); + + // Assert + assertNotNull(result); + assertEquals(1L, result.getInventoryId()); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).findByProductId(1L); + verify(inventoryRepository).save(newInventory); + } + + @Test + void testCreateInventory_WithInvalidProduct_ShouldThrowNotFoundException() { + // Arrange + Inventory newInventory = Inventory.builder() + .productId(999L) + .quantity(50) + .reservedQuantity(0) + .build(); + + when(productServiceClient.getProductById(999L)).thenReturn(null); + + // Act & Assert + InventoryNotFoundException exception = assertThrows( + InventoryNotFoundException.class, + () -> inventoryService.createInventory(newInventory) + ); + + assertTrue(exception.getMessage().contains("Product not found in catalog with ID: 999")); + verify(productServiceClient).getProductById(999L); + verify(inventoryRepository, never()).save(any()); + } + + @Test + void testCreateInventory_WithExistingInventory_ShouldThrowConflictException() { + // Arrange + Inventory newInventory = Inventory.builder() + .productId(1L) + .quantity(50) + .reservedQuantity(0) + .build(); + + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); + + // Act & Assert + InventoryConflictException exception = assertThrows( + InventoryConflictException.class, + () -> inventoryService.createInventory(newInventory) + ); + + assertTrue(exception.getMessage().contains("Inventory for product already exists")); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).findByProductId(1L); + verify(inventoryRepository, never()).save(any()); + } + + @Test + void testUpdateInventory_WithValidProduct_ShouldSucceed() { + // Arrange + Inventory updatedInventory = Inventory.builder() + .productId(1L) + .quantity(75) + .reservedQuantity(5) + .build(); + + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); + when(inventoryRepository.update(1L, updatedInventory)).thenReturn(Optional.of(mockInventory)); + + // Act + Inventory result = inventoryService.updateInventory(1L, updatedInventory); + + // Assert + assertNotNull(result); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).findByProductId(1L); + verify(inventoryRepository).update(1L, updatedInventory); + } + + @Test + void testUpdateInventory_WithProductConflict_ShouldThrowConflictException() { + // Arrange + Inventory existingInventory = Inventory.builder() + .inventoryId(2L) + .productId(1L) + .quantity(25) + .reservedQuantity(0) + .build(); + + Inventory updatedInventory = Inventory.builder() + .productId(1L) + .quantity(75) + .reservedQuantity(5) + .build(); + + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(existingInventory)); + + // Act & Assert + InventoryConflictException exception = assertThrows( + InventoryConflictException.class, + () -> inventoryService.updateInventory(1L, updatedInventory) + ); + + assertTrue(exception.getMessage().contains("Another inventory record already exists")); + verify(productServiceClient).getProductById(1L); + verify(inventoryRepository).findByProductId(1L); + verify(inventoryRepository, never()).update(anyLong(), any()); + } + + @Test + void testCreateBulkInventories_WithValidProducts_ShouldSucceed() { + // Arrange + Product product2 = new Product(2L, "Product 2", 19.99, "Home", "Another product"); + + Inventory inventory1 = Inventory.builder().productId(1L).quantity(50).reservedQuantity(0).build(); + Inventory inventory2 = Inventory.builder().productId(2L).quantity(25).reservedQuantity(0).build(); + List inventories = Arrays.asList(inventory1, inventory2); + + Inventory saved1 = Inventory.builder().inventoryId(1L).productId(1L).quantity(50).reservedQuantity(0).build(); + Inventory saved2 = Inventory.builder().inventoryId(2L).productId(2L).quantity(25).reservedQuantity(0).build(); + + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + when(productServiceClient.getProductById(2L)).thenReturn(product2); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.empty()); + when(inventoryRepository.findByProductId(2L)).thenReturn(Optional.empty()); + when(inventoryRepository.save(inventory1)).thenReturn(saved1); + when(inventoryRepository.save(inventory2)).thenReturn(saved2); + + // Act + List result = inventoryService.createBulkInventories(inventories); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + verify(productServiceClient).getProductById(1L); + verify(productServiceClient).getProductById(2L); + verify(inventoryRepository, times(2)).save(any(Inventory.class)); + } + + @Test + void testGetInventoryWithProductInfo_ShouldReturnEnrichedData() { + // Arrange + when(inventoryRepository.findById(1L)).thenReturn(Optional.of(mockInventory)); + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + + // Act + InventoryWithProductInfo result = inventoryService.getInventoryWithProductInfo(1L); + + // Assert + assertNotNull(result); + assertEquals(mockInventory, result.getInventory()); + assertEquals(mockProduct, result.getProduct()); + assertEquals("Test Product", result.getProductName()); + assertEquals(29.99, result.getProductPrice()); + assertEquals("Electronics", result.getProductCategory()); + assertEquals(100, result.getQuantity()); + assertEquals(10, result.getReservedQuantity()); + assertEquals(90, result.getAvailableQuantity()); + } + + @Test + void testGetInventoriesByCategory_ShouldReturnFilteredInventories() { + // Arrange + Product product2 = new Product(2L, "Product 2", 39.99, "Electronics", "Another electronics product"); + List electronicsProducts = Arrays.asList(mockProduct, product2); + + Inventory inventory2 = Inventory.builder() + .inventoryId(2L) + .productId(2L) + .quantity(75) + .reservedQuantity(5) + .build(); + + when(productServiceClient.getProductsByCategory("Electronics")).thenReturn(electronicsProducts); + when(inventoryRepository.findByProductId(1L)).thenReturn(Optional.of(mockInventory)); + when(inventoryRepository.findByProductId(2L)).thenReturn(Optional.of(inventory2)); + + // Act + List result = inventoryService.getInventoriesByCategory("Electronics"); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + InventoryWithProductInfo first = result.get(0); + assertEquals("Test Product", first.getProductName()); + assertEquals("Electronics", first.getProductCategory()); + + InventoryWithProductInfo second = result.get(1); + assertEquals("Product 2", second.getProductName()); + assertEquals("Electronics", second.getProductCategory()); + + verify(productServiceClient).getProductsByCategory("Electronics"); + verify(inventoryRepository).findByProductId(1L); + verify(inventoryRepository).findByProductId(2L); + } + + @Test + void testGetProductInfo_ShouldReturnProductDetails() { + // Arrange + when(productServiceClient.getProductById(1L)).thenReturn(mockProduct); + + // Act + Product result = inventoryService.getProductInfo(mockInventory); + + // Assert + assertNotNull(result); + assertEquals("Test Product", result.getName()); + assertEquals(29.99, result.getPrice()); + assertEquals("Electronics", result.getCategory()); + verify(productServiceClient).getProductById(1L); + } + + @Test + void testValidateProductExists_WithServiceError_ShouldThrowRuntimeException() { + // Arrange + WebApplicationException serviceException = new WebApplicationException( + Response.status(500).build() + ); + when(productServiceClient.getProductById(1L)).thenThrow(serviceException); + + // Act & Assert + RuntimeException exception = assertThrows( + RuntimeException.class, + () -> inventoryService.createInventory(mockInventory) + ); + + assertTrue(exception.getMessage().contains("Failed to validate product with catalog service")); + verify(productServiceClient).getProductById(1L); + } +} diff --git a/code/chapter11/order/Dockerfile b/code/chapter11/order/Dockerfile new file mode 100644 index 00000000..68549649 --- /dev/null +++ b/code/chapter11/order/Dockerfile @@ -0,0 +1,19 @@ +FROM openliberty/open-liberty:23.0.0.3-full-java17-openj9-ubi + +# Copy Liberty configuration +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Copy application WAR file +COPY --chown=1001:0 target/order.war /config/apps/ + +# Set environment variables +ENV PORT=9080 + +# Configure the server to run +RUN configure.sh + +# Expose ports +EXPOSE 8050 8051 + +# Start the server +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/order/README.md b/code/chapter11/order/README.md new file mode 100644 index 00000000..e8202c7c --- /dev/null +++ b/code/chapter11/order/README.md @@ -0,0 +1,147 @@ +# Order Service + +A Jakarta EE and MicroProfile-based REST service for order management in the Liberty Rest App demo. + +## Features + +- Provides CRUD operations for order management +- Tracks orders with order_id, user_id, total_price, and status +- Manages order items with order_item_id, order_id, product_id, quantity, and price_at_order +- Uses Jakarta EE 10.0 and MicroProfile 6.1 +- Runs on Open Liberty runtime + +## Running the Application + +There are multiple ways to run the application: + +### Using Maven + +``` +cd order +mvn liberty:run +``` + +### Using the provided script + +``` +./run.sh +``` + +### Using Docker + +``` +./run-docker.sh +``` + +This will start the Open Liberty server on port 8050 (HTTP) and 8051 (HTTPS). + +## API Endpoints + +| Method | URL | Description | +|--------|:----------------------------------------|:-------------------------------------| +| GET | /api/orders | Get all orders | +| GET | /api/orders/{id} | Get order by ID | +| GET | /api/orders/user/{userId} | Get orders by user ID | +| GET | /api/orders/status/{status} | Get orders by status | +| POST | /api/orders | Create new order | +| PUT | /api/orders/{id} | Update order | +| DELETE | /api/orders/{id} | Delete order | +| PATCH | /api/orders/{id}/status/{status} | Update order status | +| GET | /api/orders/{orderId}/items | Get items for an order | +| GET | /api/orders/items/{orderItemId} | Get specific order item | +| POST | /api/orders/{orderId}/items | Add item to order | +| PUT | /api/orders/items/{orderItemId} | Update order item | +| DELETE | /api/orders/items/{orderItemId} | Delete order item | + +## Testing with cURL + +### Create new order +``` +curl -X POST http://localhost:8050/order/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "totalPrice": 149.98, + "status": "CREATED", + "orderItems": [ + { + "productId": 101, + "quantity": 2, + "priceAtOrder": 49.99 + }, + { + "productId": 102, + "quantity": 1, + "priceAtOrder": 50.00 + } + ] + }' +``` + +### Get all orders +``` +curl -X GET http://localhost:8050/order/api/orders +``` + +### Get order by ID +``` +curl -X GET http://localhost:8050/order/api/orders/1 +``` + +### Get orders by user ID +``` +curl -X GET http://localhost:8050/order/api/orders/user/1 +``` + +### Update order +``` +curl -X PUT http://localhost:8050/order/api/orders/1 \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "totalPrice": 149.98, + "status": "PAID" + }' +``` +### Get items for an order +``` +curl -X GET http://localhost:8050/order/api/orders/1/items +``` + +### Add item to order +``` +curl -X POST http://localhost:8050/order/api/orders/1/items \ + -H "Content-Type: application/json" \ + -d '{ + "productId": 103, + "quantity": 1, + "priceAtOrder": 29.99 + }' +``` + +### Update order item +``` +curl -X PUT http://localhost:8050/order/api/orders/items/1 \ + -H "Content-Type: application/json" \ + -d '{ + "orderId": 1, + "productId": 103, + "quantity": 2, + "priceAtOrder": 29.99 + }' +``` + +### Update order status +``` +curl -X PATCH http://localhost:8050/order/api/orders/1/status/SHIPPED +``` + +### Delete order item +``` +curl -X DELETE http://localhost:8050/order/api/orders/items/1 +``` + +### Delete order +``` +curl -X DELETE http://localhost:8050/order/api/orders/1 +``` diff --git a/code/chapter11/order/pom.xml b/code/chapter11/order/pom.xml new file mode 100644 index 00000000..9e702791 --- /dev/null +++ b/code/chapter11/order/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.microprofile + order + 1.0-SNAPSHOT + war + + order-management + https://microprofile.io + + + UTF-8 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + order + + + + io.openliberty.tools + liberty-maven-plugin + 3.8.2 + + orderServer + runnable + 120 + + /order + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/order/restart-server.sh b/code/chapter11/order/restart-server.sh new file mode 100644 index 00000000..cf673ccb --- /dev/null +++ b/code/chapter11/order/restart-server.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Script to restart the Liberty server and test the Swagger UI + +# Change to the order directory +cd /workspaces/liberty-rest-app/order + +# Build the application +echo "Building the Order Service application..." +mvn clean package + +# Stop the Liberty server +echo "Stopping Liberty server..." +mvn liberty:stop + +# Copy the public key to the Liberty server config directory +echo "Copying public key to Liberty config directory..." +mkdir -p target/liberty/wlp/usr/servers/orderServer +cp src/main/resources/META-INF/publicKey.pem target/liberty/wlp/usr/servers/orderServer/ + +# Start the Liberty server +echo "Starting Liberty server..." +mvn liberty:start + +# Wait for the server to start +echo "Waiting for server to start..." +sleep 10 + +# Print URLs for testing +echo "" +echo "Server started. You can access the following URLs:" +echo "- API Documentation: http://localhost:8050/order/openapi/ui" +echo "- Custom Swagger UI: http://localhost:8050/order/swagger.html" +echo "- Home Page: http://localhost:8050/order/index.html" +echo "" +echo "If Swagger UI has CORS issues, use the custom Swagger UI at /swagger.html" diff --git a/code/chapter11/order/run-docker.sh b/code/chapter11/order/run-docker.sh new file mode 100644 index 00000000..c3d89128 --- /dev/null +++ b/code/chapter11/order/run-docker.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Build the application +mvn clean package + +# Build the Docker image +docker build -t order-service . + +# Run the container +docker run -d --name order-service -p 8050:8050 -p 8051:8051 order-service diff --git a/code/chapter11/order/run.sh b/code/chapter11/order/run.sh new file mode 100644 index 00000000..7b7db548 --- /dev/null +++ b/code/chapter11/order/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Navigate to the order service directory +cd "$(dirname "$0")" + +# Build the project +echo "Building Order Service..." +mvn clean package + +# Run the Liberty server +echo "Starting Order Service..." +mvn liberty:run diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java new file mode 100644 index 00000000..3113aac8 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/OrderApplication.java @@ -0,0 +1,34 @@ +package io.microprofile.tutorial.store.order; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.info.Contact; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.License; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * JAX-RS application for order management. + */ +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Order API", + version = "1.0.0", + description = "API for managing orders and order items", + license = @License( + name = "Eclipse Public License 2.0", + url = "https://www.eclipse.org/legal/epl-2.0/"), + contact = @Contact( + name = "Order API Support", + email = "support@example.com")), + tags = { + @Tag(name = "Order", description = "Operations related to order management"), + @Tag(name = "OrderItem", description = "Operations related to order item management") + } +) +public class OrderApplication extends Application { + // The resources will be discovered automatically +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java new file mode 100644 index 00000000..c1d8be1c --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/Order.java @@ -0,0 +1,45 @@ +package io.microprofile.tutorial.store.order.entity; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Order class for the microprofile tutorial store application. + * This class represents an order in the system with its details. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Order { + + private Long orderId; + + @NotNull(message = "User ID cannot be null") + private Long userId; + + @NotNull(message = "Total price cannot be null") + @Min(value = 0, message = "Total price must be greater than or equal to 0") + private BigDecimal totalPrice; + + @NotNull(message = "Status cannot be null") + private OrderStatus status; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + @Builder.Default + private List orderItems = new ArrayList<>(); +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java new file mode 100644 index 00000000..ef849969 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderItem.java @@ -0,0 +1,38 @@ +package io.microprofile.tutorial.store.order.entity; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +/** + * OrderItem class for the microprofile tutorial store application. + * This class represents an item within an order. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OrderItem { + + private Long orderItemId; + + @NotNull(message = "Order ID cannot be null") + private Long orderId; + + @NotNull(message = "Product ID cannot be null") + private Long productId; + + @NotNull(message = "Quantity cannot be null") + @Min(value = 1, message = "Quantity must be at least 1") + private Integer quantity; + + @NotNull(message = "Price at order cannot be null") + @Min(value = 0, message = "Price must be greater than or equal to 0") + private BigDecimal priceAtOrder; +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java new file mode 100644 index 00000000..af04ec26 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/entity/OrderStatus.java @@ -0,0 +1,14 @@ +package io.microprofile.tutorial.store.order.entity; + +/** + * OrderStatus enum for the microprofile tutorial store application. + * This enum defines the possible statuses for an order. + */ +public enum OrderStatus { + CREATED, + PAID, + PROCESSING, + SHIPPED, + DELIVERED, + CANCELLED +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java new file mode 100644 index 00000000..9c72ad80 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/package-info.java @@ -0,0 +1,14 @@ +/** + * This package contains the Order Management application for the MicroProfile tutorial store. + * + * The application demonstrates a Jakarta EE and MicroProfile-based REST service + * for managing orders and order items with CRUD operations. + * + * Main Components: + * - Entity classes: Contains order data with order_id, user_id, total_price, status + * and order item data with order_item_id, order_id, product_id, quantity, price_at_order + * - Repository: Provides in-memory data storage using HashMap + * - Service: Contains business logic and validation + * - Resource: REST endpoints with OpenAPI documentation + */ +package io.microprofile.tutorial.store.order; diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java new file mode 100644 index 00000000..1aa11cf6 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderItemRepository.java @@ -0,0 +1,124 @@ +package io.microprofile.tutorial.store.order.repository; + +import io.microprofile.tutorial.store.order.entity.OrderItem; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Simple in-memory repository for OrderItem objects. + * This class provides CRUD operations for OrderItem entities to demonstrate MicroProfile concepts. + */ +@ApplicationScoped +public class OrderItemRepository { + + private final Map orderItems = new HashMap<>(); + private long nextId = 1; + + /** + * Saves an order item to the repository. + * If the order item has no ID, a new ID is assigned. + * + * @param orderItem The order item to save + * @return The saved order item with ID assigned + */ + public OrderItem save(OrderItem orderItem) { + if (orderItem.getOrderItemId() == null) { + orderItem.setOrderItemId(nextId++); + } + orderItems.put(orderItem.getOrderItemId(), orderItem); + return orderItem; + } + + /** + * Finds an order item by ID. + * + * @param id The order item ID + * @return An Optional containing the order item if found, or empty if not found + */ + public Optional findById(Long id) { + return Optional.ofNullable(orderItems.get(id)); + } + + /** + * Finds order items by order ID. + * + * @param orderId The order ID + * @return A list of order items for the specified order + */ + public List findByOrderId(Long orderId) { + return orderItems.values().stream() + .filter(item -> item.getOrderId().equals(orderId)) + .collect(Collectors.toList()); + } + + /** + * Finds order items by product ID. + * + * @param productId The product ID + * @return A list of order items for the specified product + */ + public List findByProductId(Long productId) { + return orderItems.values().stream() + .filter(item -> item.getProductId().equals(productId)) + .collect(Collectors.toList()); + } + + /** + * Retrieves all order items from the repository. + * + * @return A list of all order items + */ + public List findAll() { + return new ArrayList<>(orderItems.values()); + } + + /** + * Deletes an order item by ID. + * + * @param id The ID of the order item to delete + * @return true if the order item was deleted, false if not found + */ + public boolean deleteById(Long id) { + return orderItems.remove(id) != null; + } + + /** + * Deletes all order items for an order. + * + * @param orderId The ID of the order + * @return The number of order items deleted + */ + public int deleteByOrderId(Long orderId) { + List itemsToDelete = orderItems.values().stream() + .filter(item -> item.getOrderId().equals(orderId)) + .map(OrderItem::getOrderItemId) + .collect(Collectors.toList()); + + itemsToDelete.forEach(orderItems::remove); + return itemsToDelete.size(); + } + + /** + * Updates an existing order item. + * + * @param id The ID of the order item to update + * @param orderItem The updated order item information + * @return An Optional containing the updated order item, or empty if not found + */ + public Optional update(Long id, OrderItem orderItem) { + if (!orderItems.containsKey(id)) { + return Optional.empty(); + } + + orderItem.setOrderItemId(id); + orderItems.put(id, orderItem); + return Optional.of(orderItem); + } +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java new file mode 100644 index 00000000..743bd26d --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/repository/OrderRepository.java @@ -0,0 +1,109 @@ +package io.microprofile.tutorial.store.order.repository; + +import io.microprofile.tutorial.store.order.entity.Order; +import io.microprofile.tutorial.store.order.entity.OrderStatus; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Simple in-memory repository for Order objects. + * This class provides CRUD operations for Order entities to demonstrate MicroProfile concepts. + */ +@ApplicationScoped +public class OrderRepository { + + private final Map orders = new HashMap<>(); + private long nextId = 1; + + /** + * Saves an order to the repository. + * If the order has no ID, a new ID is assigned. + * + * @param order The order to save + * @return The saved order with ID assigned + */ + public Order save(Order order) { + if (order.getOrderId() == null) { + order.setOrderId(nextId++); + } + orders.put(order.getOrderId(), order); + return order; + } + + /** + * Finds an order by ID. + * + * @param id The order ID + * @return An Optional containing the order if found, or empty if not found + */ + public Optional findById(Long id) { + return Optional.ofNullable(orders.get(id)); + } + + /** + * Finds orders by user ID. + * + * @param userId The user ID + * @return A list of orders for the specified user + */ + public List findByUserId(Long userId) { + return orders.values().stream() + .filter(order -> order.getUserId().equals(userId)) + .collect(Collectors.toList()); + } + + /** + * Finds orders by status. + * + * @param status The order status + * @return A list of orders with the specified status + */ + public List findByStatus(OrderStatus status) { + return orders.values().stream() + .filter(order -> order.getStatus().equals(status)) + .collect(Collectors.toList()); + } + + /** + * Retrieves all orders from the repository. + * + * @return A list of all orders + */ + public List findAll() { + return new ArrayList<>(orders.values()); + } + + /** + * Deletes an order by ID. + * + * @param id The ID of the order to delete + * @return true if the order was deleted, false if not found + */ + public boolean deleteById(Long id) { + return orders.remove(id) != null; + } + + /** + * Updates an existing order. + * + * @param id The ID of the order to update + * @param order The updated order information + * @return An Optional containing the updated order, or empty if not found + */ + public Optional update(Long id, Order order) { + if (!orders.containsKey(id)) { + return Optional.empty(); + } + + order.setOrderId(id); + orders.put(id, order); + return Optional.of(order); + } +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java new file mode 100644 index 00000000..e20d36f5 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderItemResource.java @@ -0,0 +1,149 @@ +package io.microprofile.tutorial.store.order.resource; + +import io.microprofile.tutorial.store.order.entity.OrderItem; +import io.microprofile.tutorial.store.order.service.OrderService; + +import java.net.URI; +import java.util.List; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * REST resource for order item operations. + */ +@Path("/orderItems") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "OrderItem", description = "Operations related to order item management") +public class OrderItemResource { + + @Inject + private OrderService orderService; + + @Context + private UriInfo uriInfo; + + @GET + @Path("/{id}") + @Operation(summary = "Get order item by ID", description = "Returns a specific order item by ID") + @APIResponse( + responseCode = "200", + description = "Order item", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrderItem.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order item not found" + ) + public OrderItem getOrderItemById( + @Parameter(description = "ID of the order item", required = true) + @PathParam("id") Long id) { + return orderService.getOrderItemById(id); + } + + @GET + @Path("/order/{orderId}") + @Operation(summary = "Get order items by order ID", description = "Returns items for a specific order") + @APIResponse( + responseCode = "200", + description = "List of order items", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = OrderItem.class) + ) + ) + public List getOrderItemsByOrderId( + @Parameter(description = "Order ID", required = true) + @PathParam("orderId") Long orderId) { + return orderService.getOrderItemsByOrderId(orderId); + } + + @POST + @Path("/order/{orderId}") + @Operation(summary = "Add item to order", description = "Adds a new item to an existing order") + @APIResponse( + responseCode = "201", + description = "Order item added", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrderItem.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order not found" + ) + public Response addOrderItem( + @Parameter(description = "ID of the order", required = true) + @PathParam("orderId") Long orderId, + @Parameter(description = "Order item details", required = true) + @NotNull @Valid OrderItem orderItem) { + OrderItem createdItem = orderService.addOrderItem(orderId, orderItem); + URI location = uriInfo.getBaseUriBuilder() + .path(OrderItemResource.class) + .path(createdItem.getOrderItemId().toString()) + .build(); + return Response.created(location).entity(createdItem).build(); + } + + @PUT + @Path("/{id}") + @Operation(summary = "Update order item", description = "Updates an existing order item") + @APIResponse( + responseCode = "200", + description = "Order item updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrderItem.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order item not found" + ) + public OrderItem updateOrderItem( + @Parameter(description = "ID of the order item", required = true) + @PathParam("id") Long id, + @Parameter(description = "Updated order item details", required = true) + @NotNull @Valid OrderItem orderItem) { + return orderService.updateOrderItem(id, orderItem); + } + + @DELETE + @Path("/{id}") + @Operation(summary = "Delete order item", description = "Deletes an order item") + @APIResponse( + responseCode = "204", + description = "Order item deleted" + ) + @APIResponse( + responseCode = "404", + description = "Order item not found" + ) + public Response deleteOrderItem( + @Parameter(description = "ID of the order item", required = true) + @PathParam("id") Long id) { + orderService.deleteOrderItem(id); + return Response.noContent().build(); + } +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java new file mode 100644 index 00000000..955b0442 --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/resource/OrderResource.java @@ -0,0 +1,208 @@ +package io.microprofile.tutorial.store.order.resource; + +import io.microprofile.tutorial.store.order.entity.Order; +import io.microprofile.tutorial.store.order.entity.OrderStatus; +import io.microprofile.tutorial.store.order.service.OrderService; + +import java.net.URI; +import java.util.List; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * REST resource for order operations. + */ +@Path("/orders") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Order", description = "Operations related to order management") +public class OrderResource { + + @Inject + private OrderService orderService; + + @Context + private UriInfo uriInfo; + + @GET + @Operation(summary = "Get all orders", description = "Returns a list of all orders with their items") + @APIResponse( + responseCode = "200", + description = "List of orders", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) + ) + ) + public List getAllOrders() { + return orderService.getAllOrders(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Get order by ID", description = "Returns a specific order by ID with its items") + @APIResponse( + responseCode = "200", + description = "Order", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Order.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order not found" + ) + public Order getOrderById( + @Parameter(description = "ID of the order", required = true) + @PathParam("id") Long id) { + return orderService.getOrderById(id); + } + + @GET + @Path("/user/{userId}") + @Operation(summary = "Get orders by user ID", description = "Returns orders for a specific user") + @APIResponse( + responseCode = "200", + description = "List of orders", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) + ) + ) + public List getOrdersByUserId( + @Parameter(description = "User ID", required = true) + @PathParam("userId") Long userId) { + return orderService.getOrdersByUserId(userId); + } + + @GET + @Path("/status/{status}") + @Operation(summary = "Get orders by status", description = "Returns orders with a specific status") + @APIResponse( + responseCode = "200", + description = "List of orders", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Order.class) + ) + ) + public List getOrdersByStatus( + @Parameter(description = "Order status", required = true) + @PathParam("status") String status) { + try { + OrderStatus orderStatus = OrderStatus.valueOf(status.toUpperCase()); + return orderService.getOrdersByStatus(orderStatus); + } catch (IllegalArgumentException e) { + throw new WebApplicationException("Invalid order status: " + status, Response.Status.BAD_REQUEST); + } + } + + @POST + @Operation(summary = "Create new order", description = "Creates a new order with items") + @APIResponse( + responseCode = "201", + description = "Order created", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Order.class) + ) + ) + public Response createOrder( + @Parameter(description = "Order details", required = true) + @NotNull @Valid Order order) { + Order createdOrder = orderService.createOrder(order); + URI location = uriInfo.getAbsolutePathBuilder().path(createdOrder.getOrderId().toString()).build(); + return Response.created(location).entity(createdOrder).build(); + } + + @PUT + @Path("/{id}") + @Operation(summary = "Update order", description = "Updates an existing order") + @APIResponse( + responseCode = "200", + description = "Order updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Order.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order not found" + ) + public Order updateOrder( + @Parameter(description = "ID of the order", required = true) + @PathParam("id") Long id, + @Parameter(description = "Updated order details", required = true) + @NotNull @Valid Order order) { + return orderService.updateOrder(id, order); + } + + @PATCH + @Path("/{id}/status/{status}") + @Operation(summary = "Update order status", description = "Updates the status of an order") + @APIResponse( + responseCode = "200", + description = "Order status updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Order.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Order not found" + ) + @APIResponse( + responseCode = "400", + description = "Invalid order status" + ) + public Order updateOrderStatus( + @Parameter(description = "ID of the order", required = true) + @PathParam("id") Long id, + @Parameter(description = "New order status", required = true) + @PathParam("status") String status) { + try { + OrderStatus orderStatus = OrderStatus.valueOf(status.toUpperCase()); + return orderService.updateOrderStatus(id, orderStatus); + } catch (IllegalArgumentException e) { + throw new WebApplicationException("Invalid order status: " + status, Response.Status.BAD_REQUEST); + } + } + + @DELETE + @Path("/{id}") + @Operation(summary = "Delete order", description = "Deletes an order and its items") + @APIResponse( + responseCode = "204", + description = "Order deleted" + ) + @APIResponse( + responseCode = "404", + description = "Order not found" + ) + public Response deleteOrder( + @Parameter(description = "ID of the order", required = true) + @PathParam("id") Long id) { + orderService.deleteOrder(id); + return Response.noContent().build(); + } +} diff --git a/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java new file mode 100644 index 00000000..5d3eb30a --- /dev/null +++ b/code/chapter11/order/src/main/java/io/microprofile/tutorial/store/order/service/OrderService.java @@ -0,0 +1,360 @@ +package io.microprofile.tutorial.store.order.service; + +import io.microprofile.tutorial.store.order.entity.Order; +import io.microprofile.tutorial.store.order.entity.OrderItem; +import io.microprofile.tutorial.store.order.entity.OrderStatus; +import io.microprofile.tutorial.store.order.repository.OrderItemRepository; +import io.microprofile.tutorial.store.order.repository.OrderRepository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +/** + * Service class for Order management operations. + */ +@ApplicationScoped +public class OrderService { + + @Inject + private OrderRepository orderRepository; + + @Inject + private OrderItemRepository orderItemRepository; + + /** + * Creates a new order with items. + * + * @param order The order to create + * @return The created order + */ + @Transactional + public Order createOrder(Order order) { + // Set default values + if (order.getStatus() == null) { + order.setStatus(OrderStatus.CREATED); + } + + order.setCreatedAt(LocalDateTime.now()); + order.setUpdatedAt(LocalDateTime.now()); + + // Calculate total price from order items if not specified + if (order.getTotalPrice() == null || order.getTotalPrice().compareTo(BigDecimal.ZERO) == 0) { + BigDecimal total = order.getOrderItems().stream() + .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + order.setTotalPrice(total); + } + + // Save the order first + Order savedOrder = orderRepository.save(order); + + // Save each order item + if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) { + for (OrderItem item : order.getOrderItems()) { + item.setOrderId(savedOrder.getOrderId()); + orderItemRepository.save(item); + } + } + + // Retrieve the complete order with items + return getOrderById(savedOrder.getOrderId()); + } + + /** + * Gets an order by ID with its items. + * + * @param id The order ID + * @return The order with its items + * @throws WebApplicationException if the order is not found + */ + public Order getOrderById(Long id) { + Order order = orderRepository.findById(id) + .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); + + // Load order items + List items = orderItemRepository.findByOrderId(id); + order.setOrderItems(items); + + return order; + } + + /** + * Gets all orders with their items. + * + * @return A list of all orders with their items + */ + public List getAllOrders() { + List orders = orderRepository.findAll(); + + // Load items for each order + for (Order order : orders) { + List items = orderItemRepository.findByOrderId(order.getOrderId()); + order.setOrderItems(items); + } + + return orders; + } + + /** + * Gets orders by user ID. + * + * @param userId The user ID + * @return A list of orders for the specified user + */ + public List getOrdersByUserId(Long userId) { + List orders = orderRepository.findByUserId(userId); + + // Load items for each order + for (Order order : orders) { + List items = orderItemRepository.findByOrderId(order.getOrderId()); + order.setOrderItems(items); + } + + return orders; + } + + /** + * Gets orders by status. + * + * @param status The order status + * @return A list of orders with the specified status + */ + public List getOrdersByStatus(OrderStatus status) { + List orders = orderRepository.findByStatus(status); + + // Load items for each order + for (Order order : orders) { + List items = orderItemRepository.findByOrderId(order.getOrderId()); + order.setOrderItems(items); + } + + return orders; + } + + /** + * Updates an order. + * + * @param id The order ID + * @param order The updated order information + * @return The updated order + * @throws WebApplicationException if the order is not found + */ + @Transactional + public Order updateOrder(Long id, Order order) { + // Check if order exists + if (!orderRepository.findById(id).isPresent()) { + throw new WebApplicationException("Order not found", Response.Status.NOT_FOUND); + } + + order.setOrderId(id); + order.setUpdatedAt(LocalDateTime.now()); + + // Handle order items if provided + if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) { + // Delete existing items for this order + orderItemRepository.deleteByOrderId(id); + + // Save new items + for (OrderItem item : order.getOrderItems()) { + item.setOrderId(id); + orderItemRepository.save(item); + } + + // Recalculate total price from order items + BigDecimal total = order.getOrderItems().stream() + .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + order.setTotalPrice(total); + } + + // Update the order + Order updatedOrder = orderRepository.update(id, order) + .orElseThrow(() -> new WebApplicationException("Failed to update order", Response.Status.INTERNAL_SERVER_ERROR)); + + // Reload items + List items = orderItemRepository.findByOrderId(id); + updatedOrder.setOrderItems(items); + + return updatedOrder; + } + + /** + * Updates the status of an order. + * + * @param id The order ID + * @param status The new status + * @return The updated order + * @throws WebApplicationException if the order is not found + */ + public Order updateOrderStatus(Long id, OrderStatus status) { + Order order = orderRepository.findById(id) + .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); + + order.setStatus(status); + order.setUpdatedAt(LocalDateTime.now()); + + Order updatedOrder = orderRepository.update(id, order) + .orElseThrow(() -> new WebApplicationException("Failed to update order status", Response.Status.INTERNAL_SERVER_ERROR)); + + // Reload items + List items = orderItemRepository.findByOrderId(id); + updatedOrder.setOrderItems(items); + + return updatedOrder; + } + + /** + * Deletes an order and its items. + * + * @param id The order ID + * @throws WebApplicationException if the order is not found + */ + @Transactional + public void deleteOrder(Long id) { + // Check if order exists + if (!orderRepository.findById(id).isPresent()) { + throw new WebApplicationException("Order not found", Response.Status.NOT_FOUND); + } + + // Delete order items first + orderItemRepository.deleteByOrderId(id); + + // Delete the order + boolean deleted = orderRepository.deleteById(id); + if (!deleted) { + throw new WebApplicationException("Failed to delete order", Response.Status.INTERNAL_SERVER_ERROR); + } + } + + /** + * Gets an order item by ID. + * + * @param id The order item ID + * @return The order item + * @throws WebApplicationException if the order item is not found + */ + public OrderItem getOrderItemById(Long id) { + return orderItemRepository.findById(id) + .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); + } + + /** + * Gets order items by order ID. + * + * @param orderId The order ID + * @return A list of order items for the specified order + */ + public List getOrderItemsByOrderId(Long orderId) { + return orderItemRepository.findByOrderId(orderId); + } + + /** + * Adds an item to an order. + * + * @param orderId The order ID + * @param orderItem The order item to add + * @return The added order item + * @throws WebApplicationException if the order is not found + */ + @Transactional + public OrderItem addOrderItem(Long orderId, OrderItem orderItem) { + // Check if order exists + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.NOT_FOUND)); + + orderItem.setOrderId(orderId); + OrderItem savedItem = orderItemRepository.save(orderItem); + + // Update order total price + List items = orderItemRepository.findByOrderId(orderId); + BigDecimal total = items.stream() + .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + order.setTotalPrice(total); + order.setUpdatedAt(LocalDateTime.now()); + orderRepository.update(orderId, order); + + return savedItem; + } + + /** + * Updates an order item. + * + * @param itemId The order item ID + * @param orderItem The updated order item + * @return The updated order item + * @throws WebApplicationException if the order item is not found + */ + @Transactional + public OrderItem updateOrderItem(Long itemId, OrderItem orderItem) { + // Check if item exists + OrderItem existingItem = orderItemRepository.findById(itemId) + .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); + + // Keep the same orderId + orderItem.setOrderItemId(itemId); + orderItem.setOrderId(existingItem.getOrderId()); + + OrderItem updatedItem = orderItemRepository.update(itemId, orderItem) + .orElseThrow(() -> new WebApplicationException("Failed to update order item", Response.Status.INTERNAL_SERVER_ERROR)); + + // Update order total price + Long orderId = updatedItem.getOrderId(); + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.INTERNAL_SERVER_ERROR)); + + List items = orderItemRepository.findByOrderId(orderId); + BigDecimal total = items.stream() + .map(item -> item.getPriceAtOrder().multiply(new BigDecimal(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + order.setTotalPrice(total); + order.setUpdatedAt(LocalDateTime.now()); + orderRepository.update(orderId, order); + + return updatedItem; + } + + /** + * Deletes an order item. + * + * @param itemId The order item ID + * @throws WebApplicationException if the order item is not found + */ + @Transactional + public void deleteOrderItem(Long itemId) { + // Check if item exists and get its orderId before deletion + OrderItem item = orderItemRepository.findById(itemId) + .orElseThrow(() -> new WebApplicationException("Order item not found", Response.Status.NOT_FOUND)); + + Long orderId = item.getOrderId(); + + // Delete the item + boolean deleted = orderItemRepository.deleteById(itemId); + if (!deleted) { + throw new WebApplicationException("Failed to delete order item", Response.Status.INTERNAL_SERVER_ERROR); + } + + // Update order total price + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new WebApplicationException("Order not found", Response.Status.INTERNAL_SERVER_ERROR)); + + List items = orderItemRepository.findByOrderId(orderId); + BigDecimal total = items.stream() + .map(i -> i.getPriceAtOrder().multiply(new BigDecimal(i.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + order.setTotalPrice(total); + order.setUpdatedAt(LocalDateTime.now()); + orderRepository.update(orderId, order); + } +} diff --git a/code/chapter11/order/src/main/webapp/WEB-INF/web.xml b/code/chapter11/order/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..6a516f16 --- /dev/null +++ b/code/chapter11/order/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,10 @@ + + + Order Management + + index.html + + diff --git a/code/chapter11/order/src/main/webapp/index.html b/code/chapter11/order/src/main/webapp/index.html new file mode 100644 index 00000000..1d427823 --- /dev/null +++ b/code/chapter11/order/src/main/webapp/index.html @@ -0,0 +1,144 @@ + + + + + + Order Management Service + + + +

Order Management Service

+

Welcome to the Order Management API, a Jakarta EE and MicroProfile demo.

+ +

Available Endpoints:

+ +
+

OpenAPI Documentation

+

GET /openapi - Access OpenAPI documentation

+ View API Documentation +
+ +

Order Operations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodURLDescription
GET/api/ordersGet all orders
GET/api/orders/{id}Get order by ID
GET/api/orders/user/{userId}Get orders by user ID
GET/api/orders/status/{status}Get orders by status
POST/api/ordersCreate new order
PUT/api/orders/{id}Update order
DELETE/api/orders/{id}Delete order
PATCH/api/orders/{id}/status/{status}Update order status
+ +

Order Item Operations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodURLDescription
GET/api/orderItems/order/{orderId}Get items for an order
GET/api/orderItems/{orderItemId}Get specific order item
POST/api/orderItems/order/{orderId}Add item to order
PUT/api/orderItems/{orderItemId}Update order item
DELETE/api/orderItems/{orderItemId}Delete order item
+ +

Example Request

+
curl -X GET http://localhost:8050/order/api/orders
+ + diff --git a/code/chapter11/order/src/main/webapp/order-status-codes.html b/code/chapter11/order/src/main/webapp/order-status-codes.html new file mode 100644 index 00000000..faed8a09 --- /dev/null +++ b/code/chapter11/order/src/main/webapp/order-status-codes.html @@ -0,0 +1,75 @@ + + + + + + Order Status Codes + + + +

Order Status Codes

+

This page describes the possible status codes for orders in the system.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Status CodeDescription
CREATEDOrder has been created but not yet processed
PAIDPayment has been received for the order
PROCESSINGOrder is being processed (items are being picked, packed, etc.)
SHIPPEDOrder has been shipped to the customer
DELIVEREDOrder has been delivered to the customer
CANCELLEDOrder has been cancelled
+ +

Return to main page

+ + diff --git a/code/chapter11/payment/Dockerfile b/code/chapter11/payment/Dockerfile new file mode 100644 index 00000000..77e6dde6 --- /dev/null +++ b/code/chapter11/payment/Dockerfile @@ -0,0 +1,20 @@ +FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi + +# Copy configuration files +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Create the apps directory and copy the application +COPY --chown=1001:0 target/payment.war /config/apps/ + +# Configure the server to run in production mode +RUN configure.sh + +# Expose the default port +EXPOSE 9050 9443 + +# Set the health check +HEALTHCHECK --start-period=60s --interval=10s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:9050/health || exit 1 + +# Run the server +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/payment/README.adoc b/code/chapter11/payment/README.adoc new file mode 100644 index 00000000..2a580965 --- /dev/null +++ b/code/chapter11/payment/README.adoc @@ -0,0 +1,266 @@ += Payment Service + +This microservice is part of the Jakarta EE 10 and MicroProfile 7.1-based e-commerce application. It handles payment processing and transaction management. + +== Features + +* Payment transaction processing +* Dynamic configuration management via MicroProfile Config +* RESTful API endpoints with JSON support +* Custom ConfigSource implementation +* OpenAPI documentation + +== Endpoints + +=== GET /payment/api/payment-config +* Returns all current payment configuration values +* Example: `GET http://localhost:9080/payment/api/payment-config` +* Response: `{"gateway.endpoint":"https://api.paymentgateway.com"}` + +=== POST /payment/api/payment-config +* Updates a payment configuration value +* Example: `POST http://localhost:9080/payment/api/payment-config` +* Request body: `{"key": "payment.gateway.endpoint", "value": "https://new-api.paymentgateway.com"}` +* Response: `{"key":"payment.gateway.endpoint","value":"https://new-api.paymentgateway.com","message":"Configuration updated successfully"}` + +=== POST /payment/api/authorize +* Processes a payment +* Example: `POST http://localhost:9080/payment/api/authorize` +* Response: `{"status":"success", "message":"Payment processed successfully."}` + +=== POST /payment/api/payment-config/process-example +* Example endpoint demonstrating payment processing with configuration +* Example: `POST http://localhost:9080/payment/api/payment-config/process-example` +* Request body: `{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}` +* Response: `{"amount":100.00,"message":"Payment processed successfully","status":"success","configUsed":{"gatewayEndpoint":"https://new-api.paymentgateway.com"}}` + +== Building and Running the Service + +=== Prerequisites + +* JDK 21 or higher +* Maven 3.13.0 or higher + +=== Local Development + +[source,bash] +---- +# Build the application +mvn clean package + +# Run the application with Liberty +mvn liberty:run +---- + +The server will start on port 9080 (HTTP) and 9081 (HTTPS). + +=== Docker + +[source,bash] +---- +# Build and run with Docker +./run-docker.sh +---- + +== Project Structure + +* `src/main/java/io/microprofile/tutorial/PaymentRestApplication.java` - Jakarta Restful web service application class +* `src/main/java/io/microprofile/tutorial/store/payment/config/` - Configuration classes +* `src/main/java/io/microprofile/tutorial/store/payment/resource/` - REST resource endpoints +* `src/main/java/io/microprofile/tutorial/store/payment/service/` - Business logic services +* `src/main/java/io/microprofile/tutorial/store/payment/entity/` - Data models +* `src/main/resources/META-INF/services/` - Service provider configuration +* `src/main/liberty/config/` - Liberty server configuration + +== Custom ConfigSource + +The Payment Service implements a custom MicroProfile ConfigSource named `PaymentServiceConfigSource` that provides payment-specific configuration with high priority (ordinal: 600). + +=== Available Configuration Properties + +[cols="1,2,2", options="header"] +|=== +|Property +|Description +|Default Value + +|payment.gateway.endpoint +|Payment gateway endpoint URL +|https://api.paymentgateway.com +|=== + +=== Testing ConfigSource Endpoints + +You can test the ConfigSource endpoints using curl or any REST client: + +[source,bash] +---- +# Get current configuration +curl -s http://localhost:9080/payment/api/payment-config | json_pp + +# Update configuration property +curl -s -X POST -H "Content-Type: application/json" \ + -d '{"key":"payment.gateway.endpoint", "value":"https://new-api.paymentgateway.com"}' \ + http://localhost:9080/payment/api/payment-config | json_pp + +# Test payment processing with the configuration +curl -s -X POST -H "Content-Type: application/json" \ + -d '{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}' \ + http://localhost:9080/payment/api/payment-config/process-example | json_pp + +# Test basic payment authorization +curl -s -X POST -H "Content-Type: application/json" \ + http://localhost:9080/payment/api/authorize | json_pp +---- + +=== Implementation Details + +The custom ConfigSource is implemented in the following classes: + +* `PaymentServiceConfigSource.java` - Implements the MicroProfile ConfigSource interface +* `PaymentConfig.java` - Utility class for accessing configuration properties + +Example usage in application code: + +[source,java] +---- +// Inject standard MicroProfile Config +@Inject +@ConfigProperty(name="payment.gateway.endpoint") +private String endpoint; + +// Or use the utility class +String gatewayUrl = PaymentConfig.getConfigProperty("payment.gateway.endpoint"); +---- + +The custom ConfigSource provides a higher priority (ordinal: 600) than system properties and environment variables, allowing for service-specific defaults while still enabling override via standard mechanisms. + +=== MicroProfile Config Sources + +MicroProfile Config uses a prioritized set of configuration sources. The payment service uses the following configuration sources in order of priority (highest to lowest): + +1. Custom ConfigSource (`PaymentServiceConfigSource`) - Ordinal: 600 +2. System properties - Ordinal: 400 +3. Environment variables - Ordinal: 300 +4. microprofile-config.properties file - Ordinal: 100 + +==== Updating Configuration Values + +You can update configuration properties through different methods: + +===== 1. Using the REST API (runtime) + +This uses the custom ConfigSource and persists only for the current server session: + +[source,bash] +---- +curl -X POST -H "Content-Type: application/json" \ + -d '{"key":"payment.gateway.endpoint", "value":"https://test-api.paymentgateway.com"}' \ + http://localhost:9080/payment/api/payment-config +---- + +===== 2. Using System Properties (startup) + +[source,bash] +---- +# Linux/macOS +mvn liberty:run -Dpayment.gateway.endpoint=https://sys-api.paymentgateway.com + +# Windows +mvn liberty:run "-Dpayment.gateway.endpoint=https://sys-api.paymentgateway.com" +---- + +===== 3. Using Environment Variables (startup) + +Environment variable names must follow the MicroProfile Config convention (uppercase with underscores): + +[source,bash] +---- +# Linux/macOS +export PAYMENT_GATEWAY_ENDPOINT=https://env-api.paymentgateway.com +mvn liberty:run + +# Windows PowerShell +$env:PAYMENT_GATEWAY_ENDPOINT="https://env-api.paymentgateway.com" +mvn liberty:run + +# Windows CMD +set PAYMENT_GATEWAY_ENDPOINT=https://env-api.paymentgateway.com +mvn liberty:run +---- + +===== 4. Using microprofile-config.properties File (build time) + +Edit the file at `src/main/resources/META-INF/microprofile-config.properties`: + +[source,properties] +---- +# Update the endpoint +payment.gateway.endpoint=https://config-api.paymentgateway.com +---- + +Then rebuild and restart the application: + +[source,bash] +---- +mvn clean package liberty:run +---- + +==== Testing Configuration Changes + +After changing a configuration property, you can verify it was updated by calling: + +[source,bash] +---- +curl http://localhost:9080/payment/api/payment-config +---- + +== Documentation + +=== OpenAPI + +The payment service automatically generates OpenAPI documentation using MicroProfile OpenAPI annotations. + +* OpenAPI UI: `http://localhost:9080/payment/api/openapi-ui/` +* OpenAPI JSON: `http://localhost:9080/payment/api/openapi` + +=== MicroProfile Config Specification + +For more information about MicroProfile Config, refer to the official documentation: + +* https://download.eclipse.org/microprofile/microprofile-config-3.1/microprofile-config-spec-3.1.html + +=== Related Resources + +* MicroProfile: https://microprofile.io/ +* Jakarta EE: https://jakarta.ee/ +* Open Liberty: https://openliberty.io/ + +== Troubleshooting + +=== Common Issues + +==== Port Conflicts + +If you encounter a port conflict when starting the server, you can change the ports in the `pom.xml` file: + +[source,xml] +---- +9080 +9081 +---- + +==== ConfigSource Not Loading + +If the custom ConfigSource is not loading, check the following: + +1. Verify the service provider configuration file exists at: + `src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource` + +2. Ensure it contains the correct fully qualified class name: + `io.microprofile.tutorial.store.payment.config.PaymentServiceConfigSource` + +==== Deployment Errors + +For CWWKZ0004E deployment errors, check the server logs at: +`target/liberty/wlp/usr/servers/mpServer/logs/messages.log` diff --git a/code/chapter11/payment/pom.xml b/code/chapter11/payment/pom.xml new file mode 100644 index 00000000..914f37e3 --- /dev/null +++ b/code/chapter11/payment/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + io.microprofile.tutorial + payment + 1.0-SNAPSHOT + war + + + + UTF-8 + 21 + 21 + + UTF-8 + UTF-8 + + + 9080 + 9081 + + payment + + + + + + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + jakarta.platform + jakarta.jakartaee-api + 10.0.0 + provided + + + + + org.eclipse.microprofile + microprofile + 7.1 + pom + provided + + + + junit + junit + 4.11 + test + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + + io.openliberty.tools + liberty-maven-plugin + 3.11.2 + + mpServer + + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + + \ No newline at end of file diff --git a/code/chapter11/payment/run-docker.sh b/code/chapter11/payment/run-docker.sh new file mode 100644 index 00000000..e027bafd --- /dev/null +++ b/code/chapter11/payment/run-docker.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script to build and run the Payment service in Docker + +# Stop execution on any error +set -e + +# Navigate to the payment service directory +cd "$(dirname "$0")" + +# Build the project with Maven +echo "Building with Maven..." +mvn clean package + +# Build the Docker image +echo "Building Docker image..." +docker build -t payment-service . + +# Run the Docker container +echo "Starting Docker container..." +docker run -d --name payment-service -p 9050:9050 payment-service + +echo "Payment service is running on http://localhost:9050/payment" diff --git a/code/chapter11/payment/run.sh b/code/chapter11/payment/run.sh new file mode 100644 index 00000000..75fc5f27 --- /dev/null +++ b/code/chapter11/payment/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Script to build and run the Payment service + +# Stop execution on any error +set -e + +echo "Building and running Payment service..." + +# Navigate to the payment service directory +cd "$(dirname "$0")" + +# Build the project with Maven +echo "Building with Maven..." +mvn clean package + +# Run the application using Liberty Maven plugin +echo "Starting Liberty server..." +mvn liberty:run diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java new file mode 100644 index 00000000..9ffd7515 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/PaymentRestApplication.java @@ -0,0 +1,9 @@ +package io.microprofile.tutorial; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/api") +public class PaymentRestApplication extends Application { + // No additional configuration is needed here +} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java new file mode 100644 index 00000000..81576184 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClient.java @@ -0,0 +1,67 @@ +package io.microprofile.tutorial.store.payment.client; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; + +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; + +import java.util.List; + +/** + * MicroProfile Rest Client interface for the Catalog/Product Service. + * + * This interface demonstrates: + * - @RegisterRestClient annotation to register as a REST client + * - @RegisterProvider to register custom exception mapper + * - configKey for external configuration via MicroProfile Config + * - Type-safe method definitions with Jakarta REST annotations + * - Automatic implementation generation by MicroProfile runtime + * - Custom error handling via ResponseExceptionMapper + * + * Configuration properties (in microprofile-config.properties): + * - catalog-service/mp-rest/url=http://localhost:5050/catalog/api + * - catalog-service/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped + * + * This interface extends AutoCloseable to support try-with-resources pattern + * when using RestClientBuilder for programmatic client creation. + */ +@RegisterRestClient(configKey = "catalog-service") +@RegisterProvider(ProductServiceResponseExceptionMapper.class) +@Path("/products") +@Produces(MediaType.APPLICATION_JSON) +public interface ProductClient extends AutoCloseable { + + /** + * Retrieves all products from the catalog service. + * + * @return List of all products + * @throws RuntimeException if service returns 5xx error + */ + @GET + List getAllProducts(); + + /** + * Retrieves a specific product by its ID. + * + * Example usage: productClient.getProductById(1L) + * Resulting HTTP request: GET /products/1 + * + * Demonstrates checked exception handling: + * - Throws ProductNotFoundException if product not found (404) + * - Method must declare this checked exception in throws clause + * + * @param id The product ID + * @return The product with the specified ID + * @throws ProductNotFoundException if product is not found (404) + */ + @GET + @Path("/{id}") + Product getProductById(@PathParam("id") Long id) throws ProductNotFoundException; +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java new file mode 100644 index 00000000..b5589227 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientWithFilters.java @@ -0,0 +1,89 @@ +package io.microprofile.tutorial.store.payment.client; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; + +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; +import io.microprofile.tutorial.store.payment.filter.BearerTokenFilter; +import io.microprofile.tutorial.store.payment.filter.CorrelationIdFilter; +import io.microprofile.tutorial.store.payment.filter.RequestLoggingFilter; +import io.microprofile.tutorial.store.payment.filter.ResponseLoggingFilter; + +import java.util.List; + +/** + * MicroProfile Rest Client with custom filters and interceptors registered. + * + * This interface demonstrates: + * - Registering multiple filters using @RegisterProvider + * - Specifying filter priority to control execution order + * - Combining authentication, logging, and tracing filters + * - How filters execute in the request/response lifecycle + * + * Filter Execution Order (Request): + * 1. BearerTokenFilter (Priority 1000 - AUTHENTICATION) + * - Adds Authorization header with Bearer token + * 2. CorrelationIdFilter (Priority 100) + * - Adds X-Correlation-ID and X-Request-ID headers + * 3. RequestLoggingFilter (Priority 300) + * - Logs complete request details + * + * Filter Execution Order (Response): + * 1. ResponseLoggingFilter (Priority 300) + * - Logs complete response details + * + * Compare this client with ProductClient to see the difference in logging output. + * + * Configuration properties (in microprofile-config.properties): + * - catalog-service-filtered/mp-rest/url=http://localhost:5050/catalog/api + * - catalog-service-filtered/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped + * - catalog-service.bearer.token (optional) + */ +@RegisterRestClient(configKey = "catalog-service-filtered") +@RegisterProvider(value = BearerTokenFilter.class, priority = 1000) +@RegisterProvider(value = CorrelationIdFilter.class, priority = 100) +@RegisterProvider(value = RequestLoggingFilter.class, priority = 300) +@RegisterProvider(value = ResponseLoggingFilter.class, priority = 300) +@Path("/products") +@Produces(MediaType.APPLICATION_JSON) +public interface ProductClientWithFilters extends AutoCloseable { + + /** + * Retrieves all products from the catalog service. + * + * When called, you will see filter execution in the logs: + * 1. BearerTokenFilter adds authentication + * 2. CorrelationIdFilter adds tracking IDs + * 3. RequestLoggingFilter logs the outgoing request + * 4. HTTP request is sent + * 5. HTTP response is received + * 6. ResponseLoggingFilter logs the incoming response + * 7. Response is deserialized to List + * + * @return List of all products + * @throws RuntimeException if service returns 5xx error + */ + @GET + List getAllProducts(); + + /** + * Retrieves a specific product by its ID. + * + * Demonstrates the complete filter chain with path parameters. + * Check the logs to see how filters handle parameterized requests. + * + * @param id The product ID + * @return The product with the specified ID + * @throws ProductNotFoundException if product is not found (404) + */ + @GET + @Path("/{id}") + Product getProductById(@PathParam("id") Long id) throws ProductNotFoundException; +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java new file mode 100644 index 00000000..095a448e --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java @@ -0,0 +1,55 @@ +package io.microprofile.tutorial.store.payment.client; + +import io.microprofile.tutorial.store.payment.dto.product.Product; +import jakarta.json.JsonArray; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +public class ProductJakartaRestClient { + public static Product[] getProductsWithJsonb(String targetUrl) { + // This method would typically make a REST call to fetch products. + // For now, we return an empty array as a placeholder. + Client client = ClientBuilder.newClient(); + Response response = client.target(targetUrl) + .request(MediaType.APPLICATION_JSON) + .get(); + + Product[] products = response.readEntity(Product[].class); + response.close(); + client.close(); + + + return products; + } + + + public static Product[] getProductsWithJsonp(String targetUrl) { + // Default URL for product service + String defaultUrl = "http://localhost:5050/products"; + Client client = ClientBuilder.newClient(); + Response response = client.target(targetUrl != null ? targetUrl : defaultUrl) + .request(MediaType.APPLICATION_JSON) + .get(); + + JsonArray jsonArray = response.readEntity(JsonArray.class); + response.close(); + client.close(); + + return collectProducts(jsonArray); + } + + private static Product[] collectProducts(JsonArray jsonArray) { + Product[] products = new Product[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); i++) { + Product product = new Product(); + product.setId(jsonArray.getJsonObject(i).getJsonNumber("id").longValue()); + product.setName(jsonArray.getJsonObject(i).getString("name")); + product.setPrice(jsonArray.getJsonObject(i).getJsonNumber("price").doubleValue()); + products[i] = product; + } + return products; + } +} + diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClientSimple.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClientSimple.java new file mode 100644 index 00000000..fdb98560 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClientSimple.java @@ -0,0 +1,53 @@ +package io.microprofile.tutorial.store.payment.client; + +import io.microprofile.tutorial.store.payment.dto.product.Product; +import jakarta.json.JsonArray; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +public class ProductJakartaRestClientSimple { + + public static Product[] getProductsWithJsonb(String targetUrl) { + // This method would typically make a REST call to fetch products. + // For now, we return an empty array as a placeholder. + Client client = ClientBuilder.newClient(); + Response response = client.target(targetUrl) + .request(MediaType.APPLICATION_JSON) + .get(); + + Product[] products = response.readEntity(Product[].class); + response.close(); + client.close(); + + return products; + } + + public static Product[] getProductsWithJsonp(String targetUrl) { + // Default URL for product service + String defaultUrl = "http://localhost:6050/products"; + Client client = ClientBuilder.newClient(); + Response response = client.target(targetUrl != null ? targetUrl : defaultUrl) + .request(MediaType.APPLICATION_JSON) + .get(); + + JsonArray jsonArray = response.readEntity(JsonArray.class); + response.close(); + client.close(); + + return collectProducts(jsonArray); + } + + private static Product[] collectProducts(JsonArray jsonArray) { + Product[] products = new Product[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); i++) { + Product product = new Product(); + product.setId(jsonArray.getJsonObject(i).getJsonNumber("id").longValue()); + product.setName(jsonArray.getJsonObject(i).getString("name")); + product.setPrice(jsonArray.getJsonObject(i).getJsonNumber("price").doubleValue()); + products[i] = product; + } + return products; + } +} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java new file mode 100644 index 00000000..c70fdeb5 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductServiceResponseExceptionMapper.java @@ -0,0 +1,140 @@ +package io.microprofile.tutorial.store.payment.client; + +import jakarta.annotation.Priority; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; +import io.microprofile.tutorial.store.payment.exception.ServiceUnavailableException; + +import java.io.InputStream; +import java.util.logging.Logger; + +/** + * ResponseExceptionMapper for ProductClient. + * + * This class demonstrates: + * - Mapping HTTP error responses to custom exceptions + * - Extracting error messages from JSON response bodies + * - Distinguishing between checked and unchecked exceptions + * - Using @Priority for mapper ordering + * + * HTTP Status Code Mappings: + * - 404 → ProductNotFoundException (checked exception) + * - 503 → ServiceUnavailableException (unchecked exception) + * - 500-599 → RuntimeException for server errors + * - 400-499 → RuntimeException for client errors + */ +@Priority(100) +public class ProductServiceResponseExceptionMapper implements ResponseExceptionMapper { + + private static final Logger LOGGER = Logger.getLogger(ProductServiceResponseExceptionMapper.class.getName()); + + /** + * Determines if this mapper shouldhandle the given response. + * + * @param status HTTP status code + * @param headers Response headers + * @return true if status >= 400 (client and server errors) + */ + @Override + public boolean handles(int status, MultivaluedMap headers) { + // Handle all error responses (4xx and 5xx) + boolean shouldHandle = status >= 400; + if (shouldHandle) { + LOGGER.info("ResponseExceptionMapper handling error response: " + status); + } + return shouldHandle; + } + + /** + * Converts HTTP response to an appropriate exception. + * + * Demonstrates: + * - Status code based exception mapping + * - JSON error message extraction + * - Checked vs unchecked exception handling + * + * @param response The HTTP response + * @return Throwable to be thrown by the REST client + */ + @Override + public Throwable toThrowable(Response response) { + int status = response.getStatus(); + String errorMessage = extractErrorMessage(response); + + LOGGER.warning(String.format("Mapping HTTP %d to exception: %s", status, errorMessage)); + + // Map specific status codes to custom exceptions + switch (status) { + case 404: + // Checked exception - only thrown if client method declares it + return new ProductNotFoundException("Product not found: " + errorMessage); + + case 503: + // Unchecked exception - always thrown + return new ServiceUnavailableException( + "Catalog service temporarily unavailable: " + errorMessage, status); + + case 500: + case 502: + case 504: + // Server errors - unchecked exceptions + return new RuntimeException( + String.format("Catalog service error (%d): %s", status, errorMessage)); + + default: + if (status >= 500) { + // Other 5xx errors + return new RuntimeException( + String.format("Server error (%d): %s", status, errorMessage)); + } else { + // Client errors (4xx) + return new RuntimeException( + String.format("Client error (%d): %s", status, errorMessage)); + } + } + } + + /** + * Extracts error message from response body. + * + * Attempts to parse JSON error response with structure: + * { "error": "...", "message": "..." } + * + * Falls back to generic message if parsing fails. + * + * @param response The HTTP response + * @return Extracted error message or default message + */ + private String extractErrorMessage(Response response) { + try { + if (response.hasEntity()) { + // Attempt to read JSON error response + InputStream entityStream = response.readEntity(InputStream.class); + JsonReader jsonReader = Json.createReader(entityStream); + JsonObject errorJson = jsonReader.readObject(); + + // Try multiple common error message field names + if (errorJson.containsKey("message")) { + return errorJson.getString("message"); + } else if (errorJson.containsKey("error")) { + return errorJson.getString("error"); + } else if (errorJson.containsKey("errorMessage")) { + return errorJson.getString("errorMessage"); + } + } + } catch (Exception e) { + // If JSON parsing fails, log and continue with default message + LOGGER.fine("Failed to parse error response as JSON: " + e.getMessage()); + } + + // Default error message + return "HTTP " + response.getStatus() + " - " + response.getStatusInfo().getReasonPhrase(); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java new file mode 100644 index 00000000..c4df4d6e --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentConfig.java @@ -0,0 +1,63 @@ +package io.microprofile.tutorial.store.payment.config; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +/** + * Utility class for accessing payment service configuration. + */ +public class PaymentConfig { + + private static final Config config = ConfigProvider.getConfig(); + + /** + * Gets a configuration property as a String. + * + * @param key the property key + * @return the property value + */ + public static String getConfigProperty(String key) { + return config.getValue(key, String.class); + } + + /** + * Gets a configuration property as a String with a default value. + * + * @param key the property key + * @param defaultValue the default value if the key doesn't exist + * @return the property value or the default value + */ + public static String getConfigProperty(String key, String defaultValue) { + return config.getOptionalValue(key, String.class).orElse(defaultValue); + } + + /** + * Gets a configuration property as an Integer. + * + * @param key the property key + * @return the property value as an Integer + */ + public static Integer getIntProperty(String key) { + return config.getValue(key, Integer.class); + } + + /** + * Gets a configuration property as a Boolean. + * + * @param key the property key + * @return the property value as a Boolean + */ + public static Boolean getBooleanProperty(String key) { + return config.getValue(key, Boolean.class); + } + + /** + * Updates a configuration property at runtime through the custom ConfigSource. + * + * @param key the property key + * @param value the property value + */ + public static void updateProperty(String key, String value) { + PaymentServiceConfigSource.setProperty(key, value); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java new file mode 100644 index 00000000..25b59a4f --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/config/PaymentServiceConfigSource.java @@ -0,0 +1,60 @@ +package io.microprofile.tutorial.store.payment.config; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Custom ConfigSource for Payment Service. + * This config source provides payment-specific configuration with high priority. + */ +public class PaymentServiceConfigSource implements ConfigSource { + + private static final Map properties = new HashMap<>(); + + private static final String NAME = "PaymentServiceConfigSource"; + private static final int ORDINAL = 600; // Higher ordinal means higher priority + + public PaymentServiceConfigSource() { + // Load payment service configurations dynamically + // This example uses hardcoded values for demonstration + properties.put("payment.gateway.endpoint", "https://api.paymentgateway.com"); + } + + @Override + public Map getProperties() { + return properties; + } + + @Override + public Set getPropertyNames() { + return properties.keySet(); + } + + @Override + public String getValue(String propertyName) { + return properties.get(propertyName); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public int getOrdinal() { + return ORDINAL; + } + + /** + * Updates a configuration property at runtime. + * + * @param key the property key + * @param value the property value + */ + public static void setProperty(String key, String value) { + properties.put(key, value); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java new file mode 100644 index 00000000..34963e2e --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java @@ -0,0 +1,72 @@ +package io.microprofile.tutorial.store.payment.demo; + +import io.microprofile.tutorial.store.payment.client.ProductJakartaRestClient; +import io.microprofile.tutorial.store.payment.client.ProductJakartaRestClientSimple; +import io.microprofile.tutorial.store.payment.dto.product.Product; + +import java.util.Arrays; +import java.util.logging.Logger; + +/** + * Example demonstrating how to use the ProductJakartaRestClientSimple.getProductsWithJsonp method. + */ +public class ProductClientRunner { + + private static final Logger LOGGER = Logger.getLogger(ProductClientRunner.class.getName()); + + public static void main(String[] args) { + + // Example 1: Call with default URL (http://localhost:5050/products) + LOGGER.info("=== Example 1: Using default URL ==="); + try { + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(null); + printProducts("Default URL", products); + } catch (Exception e) { + LOGGER.warning("Failed to fetch products with default URL: " + e.getMessage()); + } + + // Example 2: Call with custom catalog service URL + LOGGER.info("=== Example 2: Using custom catalog service URL ==="); + try { + String catalogUrl = "http://localhost:5050/catalog/api/products"; + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(catalogUrl); + printProducts("Custom catalog URL", products); + } catch (Exception e) { + LOGGER.warning("Failed to fetch products from catalog service: " + e.getMessage()); + } + + // Example 3: Call with different environment URLs + LOGGER.info("=== Example 3: Using different environment URLs ==="); + String[] environmentUrls = { + "http://localhost:5050/catalog/api/products", // Local catalog service + "http://localhost:6050/products", // Alternative port + "https://api.example.com/products" // External API + }; + + for (String url : environmentUrls) { + try { + LOGGER.info("Trying URL: " + url); + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(url); + printProducts("URL: " + url, products); + break; // Stop on first successful call + } catch (Exception e) { + LOGGER.warning("Failed to fetch from " + url + ": " + e.getMessage()); + } + } + } + + /** + * Helper method to print product information + */ + private static void printProducts(String source, Product[] products) { + LOGGER.info("Products from " + source + ":"); + if (products != null && products.length > 0) { + Arrays.stream(products) + .forEach(product -> LOGGER.info(" " + product.toString())); + LOGGER.info("Total products found: " + products.length); + } else { + LOGGER.info(" No products found"); + } + System.out.println(); // Add blank line for readability + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java new file mode 100644 index 00000000..3b588438 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/dto/product/Product.java @@ -0,0 +1,10 @@ +package io.microprofile.tutorial.store.payment.dto.product; + +import lombok.Data; + +@Data +public class Product { + public Long id; + public String name; + public Double price; +} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java new file mode 100644 index 00000000..4b62460a --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/entity/PaymentDetails.java @@ -0,0 +1,18 @@ +package io.microprofile.tutorial.store.payment.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PaymentDetails { + private String cardNumber; + private String cardHolderName; + private String expiryDate; // Format MM/YY + private String securityCode; + private BigDecimal amount; +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java new file mode 100644 index 00000000..1e406aa5 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ProductNotFoundException.java @@ -0,0 +1,31 @@ +package io.microprofile.tutorial.store.payment.exception; + +/** + * Exception thrown when a product is not found in the catalog service. + * + * This is a checked exception that must be declared in method signatures. + * Used by ResponseExceptionMapper to map 404 HTTP responses. + */ +public class ProductNotFoundException extends Exception { + + private final Long productId; + + public ProductNotFoundException(String message) { + super(message); + this.productId = null; + } + + public ProductNotFoundException(String message, Long productId) { + super(message); + this.productId = productId; + } + + public ProductNotFoundException(String message, Throwable cause) { + super(message, cause); + this.productId = null; + } + + public Long getProductId() { + return productId; + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java new file mode 100644 index 00000000..36f9a774 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/exception/ServiceUnavailableException.java @@ -0,0 +1,32 @@ +package io.microprofile.tutorial.store.payment.exception; + +/** + * Exception thrown when the catalog service is temporarily unavailable. + * + * This is an unchecked exception (RuntimeException) that can be thrown + * without being declared in method signatures. + * Used by ResponseExceptionMapper to map 503 HTTP responses. + */ +public class ServiceUnavailableException extends RuntimeException { + + private final int statusCode; + + public ServiceUnavailableException(String message) { + super(message); + this.statusCode = 503; + } + + public ServiceUnavailableException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public ServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + this.statusCode = 503; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java new file mode 100644 index 00000000..8c21bd68 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java @@ -0,0 +1,72 @@ +package io.microprofile.tutorial.store.payment.filter; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.Priorities; + +import org.eclipse.microprofile.config.ConfigProvider; + +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Client Request Filter for Bearer token authentication. + * + * This filter demonstrates: + * - Implementing authentication via ClientRequestFilter + * - Using @Priority for proper filter ordering + * - Reading configuration from MicroProfile Config + * - Adding Authorization headers to outgoing requests + * - Best practices for handling authentication tokens + * + * Priority set to Priorities.AUTHENTICATION (1000) ensures this runs + * before most other filters, as authentication should happen first. + * + * The Bearer token is retrieved from MicroProfile Config, allowing + * different tokens for different environments without code changes. + * + * Configuration property: + * - catalog-service.bearer.token (optional) + */ +@Priority(Priorities.AUTHENTICATION) +public class BearerTokenFilter implements ClientRequestFilter { + + private static final Logger LOGGER = Logger.getLogger(BearerTokenFilter.class.getName()); + private static final String TOKEN_CONFIG_KEY = "catalog-service.bearer.token"; + + /** + * Intercepts outgoing HTTP requests to add Bearer token authentication. + * + * The filter: + * 1. Retrieves the Bearer token from MicroProfile Config + * 2. If token is configured, adds Authorization header with Bearer scheme + * 3. Logs authentication status (without exposing the token) + * + * If no token is configured, the request proceeds without authentication. + * This allows the same code to work in both authenticated and + * non-authenticated environments. + */ + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + // Retrieve Bearer token from configuration + String bearerToken = ConfigProvider.getConfig() + .getOptionalValue(TOKEN_CONFIG_KEY, String.class) + .orElse(null); + + if (bearerToken != null && !bearerToken.isEmpty()) { + // Add Authorization header with Bearer token + requestContext.getHeaders() + .putSingle("Authorization", "Bearer " + bearerToken); + + LOGGER.info("Bearer token authentication added to request"); + + // Log token length (not the actual token for security) + LOGGER.fine(String.format("Token length: %d characters", bearerToken.length())); + + } else { + // No token configured - proceed without authentication + LOGGER.fine("No Bearer token configured - request sent without authentication"); + } + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java new file mode 100644 index 00000000..6414c309 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java @@ -0,0 +1,77 @@ +package io.microprofile.tutorial.store.payment.filter; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MultivaluedMap; + +import java.io.IOException; +import java.util.UUID; +import java.util.logging.Logger; + +/** + * Client Request Filter that adds or propagates correlation IDs for distributed tracing. + * + * This filter demonstrates: + * - Implementing ClientRequestFilter interface + * - Using @Priority to control filter execution order + * - Adding custom headers to outgoing requests + * - Generating UUIDs for request tracking + * - Header propagation patterns in microservices + * + * Correlation IDs enable: + * - Tracing requests across multiple microservices + * - Debugging distributed transactions + * - Correlating logs from different services + * - Performance monitoring and analytics + * + * Priority 100 ensures this runs early in the filter chain. + */ +@Priority(100) +public class CorrelationIdFilter implements ClientRequestFilter { + + private static final Logger LOGGER = Logger.getLogger(CorrelationIdFilter.class.getName()); + private static final String CORRELATION_ID_HEADER = "X-Correlation-ID"; + private static final String REQUEST_ID_HEADER = "X-Request-ID"; + + /** + * Intercepts outgoing HTTP requests to add correlation tracking headers. + * + * The filter: + * 1. Checks if X-Correlation-ID already exists in the request + * 2. If not present, generates a new UUID for the correlation ID + * 3. Always generates a unique request ID for this specific request + * 4. Adds both headers to the outgoing HTTP request + * + * This enables end-to-end request tracing across microservices. + */ + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + MultivaluedMap headers = requestContext.getHeaders(); + + // Check if correlation ID already exists (propagated from incoming request) + String correlationId = (String) headers.getFirst(CORRELATION_ID_HEADER); + + if (correlationId == null || correlationId.isEmpty()) { + // Generate new correlation ID for this request chain + correlationId = UUID.randomUUID().toString(); + headers.putSingle(CORRELATION_ID_HEADER, correlationId); + LOGGER.info("Generated new Correlation ID: " + correlationId); + } else { + LOGGER.info("Propagating existing Correlation ID: " + correlationId); + } + + // Always generate a unique request ID for this specific HTTP request + String requestId = UUID.randomUUID().toString(); + headers.putSingle(REQUEST_ID_HEADER, requestId); + + LOGGER.fine(String.format("Request headers added - Correlation-ID: %s, Request-ID: %s", + correlationId, requestId)); + + // Log request details with correlation context + LOGGER.info(String.format("[%s] Outgoing request: %s %s", + correlationId, + requestContext.getMethod(), + requestContext.getUri())); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java new file mode 100644 index 00000000..3c8f395e --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java @@ -0,0 +1,100 @@ +package io.microprofile.tutorial.store.payment.filter; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MultivaluedMap; + +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Client Request Filter for comprehensive logging of outgoing HTTP requests. + * + * This filter demonstrates: + * - Implementing ClientRequestFilter for request logging + * - Using @Priority to control when the filter executes + * - Accessing request metadata (method, URI, headers) + * - Best practices for logging in filters + * - Conditional header logging to avoid sensitive data exposure + * + * Priority 300 ensures this runs after authentication and correlation ID filters, + * so all headers are present and can be logged. + * + * SECURITY NOTE: Be cautious when logging headers. Sensitive data like + * Authorization tokens should never be fully logged in production. + */ +@Priority(300) +public class RequestLoggingFilter implements ClientRequestFilter { + + private static final Logger LOGGER = Logger.getLogger(RequestLoggingFilter.class.getName()); + + // Headers that should not be logged in full (for security) + private static final String[] SENSITIVE_HEADERS = { + "Authorization", "X-API-Key", "Cookie", "Set-Cookie" + }; + + /** + * Intercepts outgoing HTTP requests for comprehensive logging. + * + * Logs: + * - HTTP method and target URI + * - Request headers (with sensitive headers masked) + * - Correlation/request tracking IDs + * - Content type information + * + * This provides visibility into REST client interactions for + * debugging, monitoring, and troubleshooting. + */ + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + // Log basic request information + LOGGER.info(String.format("=== Outgoing REST Client Request ===")); + LOGGER.info(String.format("Method: %s", requestContext.getMethod())); + LOGGER.info(String.format("URI: %s", requestContext.getUri())); + + // Log media type if present + if (requestContext.getMediaType() != null) { + LOGGER.info(String.format("Content-Type: %s", requestContext.getMediaType())); + } + + // Log headers (with sensitive data masked) + MultivaluedMap headers = requestContext.getHeaders(); + LOGGER.info("Request Headers:"); + + headers.forEach((headerName, headerValues) -> { + if (isSensitiveHeader(headerName)) { + // Mask sensitive headers + LOGGER.info(String.format(" %s: [REDACTED]", headerName)); + } else { + // Log non-sensitive headers + headerValues.forEach(value -> + LOGGER.info(String.format(" %s: %s", headerName, value)) + ); + } + }); + + // Log entity class if present (for POST/PUT requests) + if (requestContext.hasEntity()) { + LOGGER.info(String.format("Entity Type: %s", + requestContext.getEntity().getClass().getSimpleName())); + } + + LOGGER.info("==================================="); + } + + /** + * Checks if a header contains sensitive information that should not be logged. + * + * @param headerName The header name to check + * @return true if the header is sensitive, false otherwise + */ + private boolean isSensitiveHeader(String headerName) { + for (String sensitiveHeader : SENSITIVE_HEADERS) { + if (sensitiveHeader.equalsIgnoreCase(headerName)) { + return true; + } + } + return false; + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java new file mode 100644 index 00000000..2a2dee20 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java @@ -0,0 +1,136 @@ +package io.microprofile.tutorial.store.payment.filter; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; + +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Client Response Filter for comprehensive logging of incoming HTTP responses. + * + * This filter demonstrates: + * - Implementing ClientResponseFilter for response logging + * - Using @Priority to control filter execution order + * - Accessing both request and response contexts + * - Logging response status, headers, and metadata + * - Correlating responses with their originating requests + * + * Priority 300 ensures this runs consistently with RequestLoggingFilter. + * + * Response filters execute AFTER the HTTP response is received but BEFORE + * entity deserialization, making them ideal for logging response metadata. + */ +@Priority(300) +public class ResponseLoggingFilter implements ClientResponseFilter { + + private static final Logger LOGGER = Logger.getLogger(ResponseLoggingFilter.class.getName()); + + /** + * Intercepts incoming HTTP responses for comprehensive logging. + * + * Logs: + * - Original request method and URI + * - HTTP status code and reason phrase + * - Response headers (including caching, content type) + * - Correlation IDs for tracing + * - Response timing information + * + * This provides complete visibility into REST client interactions. + * + * @param requestContext The original request context + * @param responseContext The received response context + */ + @Override + public void filter(ClientRequestContext requestContext, + ClientResponseContext responseContext) throws IOException { + + // Log basic response information + LOGGER.info(String.format("=== Incoming REST Client Response ===")); + LOGGER.info(String.format("Request: %s %s", + requestContext.getMethod(), + requestContext.getUri())); + LOGGER.info(String.format("Status: %d %s", + responseContext.getStatus(), + responseContext.getStatusInfo().getReasonPhrase())); + + // Log response content type + if (responseContext.getMediaType() != null) { + LOGGER.info(String.format("Content-Type: %s", + responseContext.getMediaType())); + } + + // Log content length if available + int contentLength = responseContext.getLength(); + if (contentLength >= 0) { + LOGGER.info(String.format("Content-Length: %d bytes", contentLength)); + } + + // Log important response headers + LOGGER.info("Response Headers:"); + + // Correlation/tracking headers + String correlationId = responseContext.getHeaderString("X-Correlation-ID"); + if (correlationId != null) { + LOGGER.info(String.format(" X-Correlation-ID: %s", correlationId)); + } + + String requestId = responseContext.getHeaderString("X-Request-ID"); + if (requestId != null) { + LOGGER.info(String.format(" X-Request-ID: %s", requestId)); + } + + // Cache control headers + String cacheControl = responseContext.getHeaderString("Cache-Control"); + if (cacheControl != null) { + LOGGER.info(String.format(" Cache-Control: %s", cacheControl)); + } + + String etag = responseContext.getHeaderString("ETag"); + if (etag != null) { + LOGGER.info(String.format(" ETag: %s", etag)); + } + + // Server information + String server = responseContext.getHeaderString("Server"); + if (server != null) { + LOGGER.info(String.format(" Server: %s", server)); + } + + // Log success or error status + if (isSuccessful(responseContext.getStatus())) { + LOGGER.info("Result: SUCCESS"); + } else if (isClientError(responseContext.getStatus())) { + LOGGER.warning(String.format("Result: CLIENT ERROR (%d)", + responseContext.getStatus())); + } else if (isServerError(responseContext.getStatus())) { + LOGGER.severe(String.format("Result: SERVER ERROR (%d)", + responseContext.getStatus())); + } + + LOGGER.info("===================================="); + } + + /** + * Checks if the HTTP status code indicates success (2xx). + */ + private boolean isSuccessful(int statusCode) { + return statusCode >= 200 && statusCode < 300; + } + + /** + * Checks if the HTTP status code indicates a client error (4xx). + */ + private boolean isClientError(int statusCode) { + return statusCode >= 400 && statusCode < 500; + } + + /** + * Checks if the HTTP status code indicates a server error (5xx). + */ + private boolean isServerError(int statusCode) { + return statusCode >= 500 && statusCode < 600; + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java new file mode 100644 index 00000000..6a4002f3 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentConfigResource.java @@ -0,0 +1,98 @@ +package io.microprofile.tutorial.store.payment.resource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.Map; + +import io.microprofile.tutorial.store.payment.config.PaymentConfig; +import io.microprofile.tutorial.store.payment.entity.PaymentDetails; + +/** + * Resource to demonstrate the use of the custom ConfigSource. + */ +@ApplicationScoped +@Path("/payment-config") +public class PaymentConfigResource { + + /** + * Get all payment configuration properties. + * + * @return Response with payment configuration + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getPaymentConfig() { + Map configValues = new HashMap<>(); + + // Retrieve values from our custom ConfigSource + configValues.put("gateway.endpoint", PaymentConfig.getConfigProperty("payment.gateway.endpoint")); + + return Response.ok(configValues).build(); + } + + /** + * Update a payment configuration property. + * + * @param configUpdate Map containing the key and value to update + * @return Response indicating success + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updatePaymentConfig(Map configUpdate) { + String key = configUpdate.get("key"); + String value = configUpdate.get("value"); + + if (key == null || value == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Both 'key' and 'value' must be provided").build(); + } + + // Only allow updating specific payment properties + if (!key.startsWith("payment.")) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Only payment configuration properties can be updated").build(); + } + + // Update the property in our custom ConfigSource + PaymentConfig.updateProperty(key, value); + + return Response.ok(Map.of("message", "Configuration updated successfully", + "key", key, "value", value)).build(); + } + + /** + * Example of how to use the payment configuration in a real payment processing method. + * + * @param paymentDetails Payment details for processing + * @return Response with payment result + */ + @POST + @Path("/process-example") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response processPaymentExample(PaymentDetails paymentDetails) { + // Using configuration values in payment processing logic + String gatewayEndpoint = PaymentConfig.getConfigProperty("payment.gateway.endpoint"); + + // This is just for demonstration - in a real implementation, + // we would use these values to configure the payment gateway client + Map result = new HashMap<>(); + result.put("status", "success"); + result.put("message", "Payment processed successfully"); + result.put("amount", paymentDetails.getAmount()); + result.put("configUsed", Map.of( + "gatewayEndpoint", gatewayEndpoint + )); + + return Response.ok(result).build(); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java new file mode 100644 index 00000000..2927e7f3 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java @@ -0,0 +1,186 @@ +package io.microprofile.tutorial.store.payment.resource; + +import io.microprofile.tutorial.store.payment.client.ProductJakartaRestClient; +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.service.ProductIntegrationService; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * REST resource demonstrating how to use ProductJakartaRestClient.getProductsWithJsonp + * within REST endpoints for the Payment service. + */ +@ApplicationScoped +@Path("/products") +public class PaymentProductResource { + + private static final Logger LOGGER = Logger.getLogger(PaymentProductResource.class.getName()); + + @Inject + private ProductIntegrationService productService; + + @Inject + @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog/api/products") + private String catalogServiceUrl; + + /** + * Gets all products available for payment processing. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get all products", description = "Retrieves all products available for payment processing") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Products retrieved successfully"), + @APIResponse(responseCode = "500", description = "Failed to retrieve products") + }) + public Response getAllProducts() { + LOGGER.info("REST: Fetching all products for payment processing"); + + try { + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); + + if (products != null) { + LOGGER.info("Successfully retrieved " + products.length + " products"); + return Response.ok(products).build(); + } else { + LOGGER.warning("No products returned from catalog service"); + return Response.ok(new Product[0]).build(); + } + + } catch (Exception e) { + LOGGER.severe("Failed to retrieve products: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Failed to retrieve products", "message", e.getMessage())) + .build(); + } + } + + /** + * Gets products from a specific catalog service URL. + */ + @GET + @Path("/from-url") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get products from specific URL", description = "Retrieves products from a specified catalog service URL") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Products retrieved successfully"), + @APIResponse(responseCode = "400", description = "Invalid URL provided"), + @APIResponse(responseCode = "500", description = "Failed to retrieve products") + }) + public Response getProductsFromUrl( + @Parameter(description = "Catalog service URL", required = true) + @QueryParam("url") String catalogUrl) { + + if (catalogUrl == null || catalogUrl.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "URL parameter is required")) + .build(); + } + + LOGGER.info("REST: Fetching products from URL: " + catalogUrl); + + try { + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogUrl); + + Map result = new HashMap<>(); + result.put("sourceUrl", catalogUrl); + result.put("productCount", products != null ? products.length : 0); + result.put("products", products != null ? products : new Product[0]); + + return Response.ok(result).build(); + + } catch (Exception e) { + LOGGER.severe("Failed to retrieve products from " + catalogUrl + ": " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "error", "Failed to retrieve products", + "url", catalogUrl, + "message", e.getMessage())) + .build(); + } + } + + /** + * Validates if a product is available for payment processing. + */ + @GET + @Path("/{productId}/validate") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Validate product for payment", description = "Validates if a product is available for payment processing") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Product validation completed"), + @APIResponse(responseCode = "404", description = "Product not found") + }) + public Response validateProduct( + @Parameter(description = "Product ID", required = true) + @PathParam("productId") Long productId) { + + LOGGER.info("REST: Validating product ID: " + productId); + + boolean isValid = productService.validateProductForPayment(productId); + Product productDetails = productService.getProductDetails(productId); + + Map result = new HashMap<>(); + result.put("productId", productId); + result.put("isValid", isValid); + result.put("availableForPayment", isValid); + + if (productDetails != null) { + result.put("product", productDetails); + } + + Response.Status status = isValid ? Response.Status.OK : Response.Status.NOT_FOUND; + return Response.status(status).entity(result).build(); + } + + /** + * Gets products within a specific price range. + */ + @GET + @Path("/price-range") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get products by price range", description = "Retrieves products within a specified price range") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Products retrieved successfully"), + @APIResponse(responseCode = "400", description = "Invalid price range") + }) + public Response getProductsByPriceRange( + @Parameter(description = "Minimum price", required = true) + @QueryParam("minPrice") @DefaultValue("0") double minPrice, + @Parameter(description = "Maximum price", required = true) + @QueryParam("maxPrice") @DefaultValue("1000") double maxPrice) { + + if (minPrice < 0 || maxPrice < 0 || minPrice > maxPrice) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Invalid price range. minPrice and maxPrice must be >= 0 and minPrice <= maxPrice")) + .build(); + } + + LOGGER.info(String.format("REST: Fetching products in price range: $%.2f - $%.2f", minPrice, maxPrice)); + + List products = productService.getProductsByPriceRange(minPrice, maxPrice); + + Map result = new HashMap<>(); + result.put("minPrice", minPrice); + result.put("maxPrice", maxPrice); + result.put("productCount", products.size()); + result.put("products", products); + + return Response.ok(result).build(); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java new file mode 100644 index 00000000..c1d62335 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/ProductCatalogResource.java @@ -0,0 +1,853 @@ +package io.microprofile.tutorial.store.payment.resource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.service.ProductCatalogService; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; +import io.microprofile.tutorial.store.payment.exception.ServiceUnavailableException; + +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * REST resource demonstrating MicroProfile Rest Client usage. + * + * This resource showcases: + * - CDI injection of service that uses REST client + * - REST endpoints that proxy calls to remote services + * - Error handling and response mapping + * - OpenAPI documentation + */ +@ApplicationScoped +@Path("/catalog") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Product Catalog", description = "REST Client demonstration endpoints") +public class ProductCatalogResource { + + private static final Logger LOGGER = Logger.getLogger(ProductCatalogResource.class.getName()); + + @Inject + ProductCatalogService catalogService; + + @Inject + io.microprofile.tutorial.store.payment.service.ProductClientBuilderService builderService; + + @Inject + io.microprofile.tutorial.store.payment.service.FilteredProductCatalogService filteredCatalogService; + + /** + * Gets all products using MicroProfile Rest Client. + * + * Demonstrates: + * - Simple GET request through REST client + * - Automatic JSON serialization + * - List response handling + */ + @GET + @Path("/products") + @Operation(summary = "Get all products", + description = "Fetches all products from the catalog service using MP Rest Client") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Products retrieved successfully"), + @APIResponse(responseCode = "503", description = "Catalog service unavailable") + }) + public Response getAllProducts() { + LOGGER.info("REST: Fetching all products via MicroProfile Rest Client"); + try { + List products = catalogService.getAllProducts(); + return Response.ok(products).build(); + } catch (Exception e) { + LOGGER.severe("Failed to fetch products: " + e.getMessage()); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(Map.of("error", "Catalog service unavailable", "message", e.getMessage())) + .build(); + } + } + + /** + * Gets a specific product by ID. + * + * Demonstrates: + * - Path parameter usage + * - Single entity response + * - 404 handling + */ + @GET + @Path("/products/{id}") + @Operation(summary = "Get product by ID", + description = "Fetches a specific product using MP Rest Client with @PathParam") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Product found"), + @APIResponse(responseCode = "404", description = "Product not found"), + @APIResponse(responseCode = "503", description = "Catalog service unavailable") + }) + public Response getProductById( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("REST: Fetching product " + id + " via MicroProfile Rest Client"); + try { + Product product = catalogService.getProduct(id); + if (product != null) { + return Response.ok(product).build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Product not found", "productId", id)) + .build(); + } + } catch (Exception e) { + LOGGER.severe("Failed to fetch product: " + e.getMessage()); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(Map.of("error", "Catalog service unavailable", "message", e.getMessage())) + .build(); + } + } + + /** + * Checks if a product is available for purchase. + * + * Demonstrates: + * - Boolean logic with REST client + * - Business validation + */ + @GET + @Path("/products/{id}/availability") + @Operation(summary = "Check product availability", + description = "Validates if a product is available for payment processing") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Availability check completed"), + @APIResponse(responseCode = "503", description = "Catalog service unavailable") + }) + public Response checkProductAvailability( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("REST: Checking availability for product " + id); + try { + boolean available = catalogService.isProductAvailable(id); + return Response.ok(Map.of( + "productId", id, + "available", available, + "message", available ? "Product is available" : "Product is not available" + )).build(); + } catch (Exception e) { + LOGGER.severe("Failed to check availability: " + e.getMessage()); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(Map.of("error", "Catalog service unavailable", "message", e.getMessage())) + .build(); + } + } + + /** + * Validates product price for payment processing. + * + * Demonstrates: + * - Query parameter usage + * - Price validation logic + */ + @GET + @Path("/products/{id}/validate-price") + @Operation(summary = "Validate product price", + description = "Validates if the product price matches expected amount") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Price validation completed"), + @APIResponse(responseCode = "400", description = "Invalid input"), + @APIResponse(responseCode = "503", description = "Catalog service unavailable") + }) + public Response validateProductPrice( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id, + @Parameter(description = "Expected price", required = true) + @QueryParam("price") Double price) { + + if (price == null || price <= 0) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Invalid price", "message", "Price must be greater than 0")) + .build(); + } + + LOGGER.info(String.format("REST: Validating price for product %d (expected: %.2f)", id, price)); + try { + boolean valid = catalogService.validateProductPrice(id, price); + return Response.ok(Map.of( + "productId", id, + "expectedPrice", price, + "valid", valid, + "message", valid ? "Price matches" : "Price mismatch detected" + )).build(); + } catch (Exception e) { + LOGGER.severe("Failed to validate price: " + e.getMessage()); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(Map.of("error", "Catalog service unavailable", "message", e.getMessage())) + .build(); + } + } + + /** + * Demonstrates exception handling with ResponseExceptionMapper. + * + * This endpoint shows how different HTTP error codes are mapped to exceptions: + * - 404 → ProductNotFoundException (checked exception) + * - 503 → ServiceUnavailableException (unchecked exception) + * - Other errors → RuntimeException + * + * Try with different IDs to see error handling: + * - Valid ID (e.g., 1) → Returns product + * - Invalid ID (e.g., 999999) → 404 ProductNotFoundException + */ + @GET + @Path("/products/{id}/detailed") + @Operation(summary = "Get product with detailed error handling", + description = "Demonstrates ResponseExceptionMapper with custom exception handling") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Product found"), + @APIResponse(responseCode = "404", description = "Product not found (handled by ResponseExceptionMapper)"), + @APIResponse(responseCode = "503", description = "Service unavailable (handled by ResponseExceptionMapper)") + }) + public Response getProductWithExceptionHandling( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("REST: Demonstrating exception handling for product " + id); + try { + Product product = catalogService.getProduct(id); + + if (product != null) { + return Response.ok(Map.of( + "productId", id, + "product", product, + "note", "Product retrieved successfully - no exceptions thrown" + )).build(); + } else { + // Product not found (404 was handled by ResponseExceptionMapper) + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of( + "productId", id, + "error", "Product not found", + "note", "ProductNotFoundException was caught and handled by the service layer" + )) + .build(); + } + + } catch (ServiceUnavailableException e) { + // Unchecked exception from ResponseExceptionMapper + LOGGER.severe("Catalog service unavailable: " + e.getMessage()); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(Map.of( + "productId", id, + "error", "Catalog service unavailable", + "statusCode", e.getStatusCode(), + "message", e.getMessage(), + "note", "ServiceUnavailableException (unchecked) was thrown by ResponseExceptionMapper" + )) + .build(); + + } catch (RuntimeException e) { + // Other runtime exceptions from ResponseExceptionMapper + LOGGER.severe("Unexpected error: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Unexpected error", + "message", e.getMessage(), + "note", "RuntimeException was thrown by ResponseExceptionMapper" + )) + .build(); + } + } + + // ==================================================================================== + // RestClientBuilder (Programmatic Client Creation) Examples + // ==================================================================================== + + /** + * Checks product availability using RestClientBuilder (programmatic creation). + * + * Demonstrates: + * - Creating REST client programmatically with RestClientBuilder + * - Try-with-resources pattern for automatic resource cleanup + * - Using baseUri(String) method (MicroProfile Rest Client 4.0) + * - Configuring timeouts programmatically + * - When to prefer programmatic over CDI injection + * + * Example: GET /catalog/builder/products/1/check + * + * @param id Product ID to check + * @return JSON response indicating if product is available + */ + @GET + @Path("/builder/products/{id}/check") + @Operation( + summary = "Check product availability (RestClientBuilder)", + description = "Uses RestClientBuilder to programmatically create a REST client and check product availability. " + + "Demonstrates try-with-resources pattern and dynamic client configuration." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Product availability check completed" + ) + }) + public Response checkProductWithBuilder( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("Checking product availability using RestClientBuilder: " + id); + + try { + boolean available = builderService.isProductAvailable(id); + return Response.ok(Map.of( + "productId", id, + "available", available, + "method", "RestClientBuilder (programmatic)", + "note", available + ? "Product found using programmatically created client" + : "Product not found - ProductNotFoundException caught" + )).build(); + + } catch (Exception e) { + LOGGER.severe("Error checking product: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Error checking product", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Gets all products using RestClientBuilder. + * + * Demonstrates: + * - Programmatic client creation for simple GET requests + * - Automatic resource cleanup with AutoCloseable interface + * - Comparison with CDI-injected client approach + * + * Example: GET /catalog/builder/products + * + * @return List of products retrieved using programmatic client + */ + @GET + @Path("/builder/products") + @Operation( + summary = "Get all products (RestClientBuilder)", + description = "Retrieves all products using a programmatically created REST client. " + + "Compare this with the CDI-injected version at /catalog/products" + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Products retrieved successfully" + ) + }) + public Response getAllProductsWithBuilder() { + LOGGER.info("Getting all products using RestClientBuilder"); + + try { + List products = builderService.getAllProducts(); + return Response.ok(Map.of( + "products", products, + "count", products.size(), + "method", "RestClientBuilder (programmatic)", + "note", "Client created with RestClientBuilder.newBuilder().baseUri(...).build()" + )).build(); + + } catch (Exception e) { + LOGGER.severe("Error getting products: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "error", "Error retrieving products", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Gets product with dynamic configuration from MicroProfile Config. + * + * Demonstrates: + * - Reading configuration at runtime with ConfigProvider + * - Dynamic client configuration based on external properties + * - Flexibility of programmatic client creation + * + * Example: GET /catalog/builder/products/1/dynamic + * + * @param id Product ID + * @return Product retrieved with dynamically configured client + */ + @GET + @Path("/builder/products/{id}/dynamic") + @Operation( + summary = "Get product with dynamic config (RestClientBuilder)", + description = "Demonstrates using RestClientBuilder with configuration loaded from MicroProfile Config at runtime. " + + "Shows how to read properties like base URL and timeouts dynamically." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Product retrieved successfully" + ), + @APIResponse( + responseCode = "404", + description = "Product not found" + ) + }) + public Response getProductWithDynamicConfig( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("Getting product with dynamic configuration: " + id); + + try { + Product product = builderService.getProductWithDynamicConfig(id); + + if (product != null) { + return Response.ok(Map.of( + "product", product, + "method", "RestClientBuilder with dynamic config", + "note", "Configuration loaded from microprofile-config.properties at runtime" + )).build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of( + "productId", id, + "error", "Product not found", + "method", "RestClientBuilder with dynamic config" + )) + .build(); + } + + } catch (Exception e) { + LOGGER.severe("Error getting product: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Error retrieving product", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Gets product for specific environment. + * + * Demonstrates: + * - Environment-specific client configuration + * - Conditional configuration based on deployment environment + * - Different timeouts for different environments + * + * Example: GET /catalog/builder/products/1/env/dev + * + * @param environment Environment name (dev, staging, prod) + * @param id Product ID + * @return Product retrieved with environment-specific configuration + */ + @GET + @Path("/builder/products/{id}/env/{environment}") + @Operation( + summary = "Get product for environment (RestClientBuilder)", + description = "Demonstrates environment-specific REST client configuration. " + + "Different base URLs and timeouts are used based on the environment parameter." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Product retrieved successfully" + ), + @APIResponse( + responseCode = "404", + description = "Product not found" + ) + }) + public Response getProductForEnvironment( + @Parameter(description = "Environment (dev, staging, prod)", required = true) + @PathParam("environment") String environment, + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info(String.format("Getting product for environment %s: %s", environment, id)); + + try { + Product product = builderService.getProductForEnvironment(environment, id); + + if (product != null) { + return Response.ok(Map.of( + "product", product, + "environment", environment, + "method", "RestClientBuilder with environment-specific config", + "note", String.format("Client configured for %s environment", environment) + )).build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of( + "productId", id, + "environment", environment, + "error", "Product not found" + )) + .build(); + } + + } catch (Exception e) { + LOGGER.severe("Error getting product: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "environment", environment, + "error", "Error retrieving product", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Checks availability of multiple products in batch. + * + * Demonstrates: + * - Creating multiple clients with different configurations + * - When programmatic creation is preferred for batch operations + * - Handling multiple requests efficiently + * - Shorter timeouts for batch processing + * + * Example: POST /catalog/builder/products/batch-check + * Body: {"productIds": [1, 2, 3, 4, 5]} + * + * @param request Request containing list of product IDs + * @return Batch check results + */ + @POST + @Path("/builder/products/batch-check") + @Operation( + summary = "Batch check product availability (RestClientBuilder)", + description = "Checks availability of multiple products using programmatically created clients. " + + "Demonstrates when programmatic creation is preferred over CDI injection." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Batch check completed" + ) + }) + public Response batchCheckProducts(Map> request) { + List productIds = request.get("productIds"); + + if (productIds == null || productIds.isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of( + "error", "Missing or empty productIds array", + "example", Map.of("productIds", List.of(1, 2, 3)) + )) + .build(); + } + + LOGGER.info("Batch checking " + productIds.size() + " products"); + + try { + int availableCount = builderService.checkMultipleProducts(productIds); + + return Response.ok(Map.of( + "totalChecked", productIds.size(), + "availableCount", availableCount, + "unavailableCount", productIds.size() - availableCount, + "method", "RestClientBuilder (batch processing)", + "note", "Each product checked with a separate programmatically created client instance" + )).build(); + + } catch (Exception e) { + LOGGER.severe("Error in batch check: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "error", "Error in batch check", + "message", e.getMessage() + )) + .build(); + } + } + + // ==================================================================================== + // Custom Filters and Interceptors Examples + // ==================================================================================== + + /** + * Gets all products using REST client with custom filters. + * + * Demonstrates: + * - ClientRequestFilter for authentication (BearerTokenFilter) + * - ClientRequestFilter for correlation tracking (CorrelationIdFilter) + * - ClientRequestFilter for request logging (RequestLoggingFilter) + * - ClientResponseFilter for response logging (ResponseLoggingFilter) + * - Filter execution order using @Priority + * + * Compare the server logs for this endpoint with /catalog/products to see + * the comprehensive logging and tracing provided by the filters. + * + * Example: GET /catalog/filtered/products + * + * @return List of products with full filter logging + */ + @GET + @Path("/filtered/products") + @Operation( + summary = "Get all products with filters", + description = "Retrieves products using a REST client with custom filters registered. " + + "Check server logs to see filter execution: BearerTokenFilter (auth), " + + "CorrelationIdFilter (tracing), RequestLoggingFilter and ResponseLoggingFilter." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Products retrieved with comprehensive filter logging" + ) + }) + public Response getAllProductsWithFilters() { + LOGGER.info("Getting all products with custom filters"); + + try { + List products = filteredCatalogService.getAllProducts(); + return Response.ok(Map.of( + "products", products, + "count", products.size(), + "method", "REST Client with Custom Filters", + "filters", List.of( + "BearerTokenFilter (Priority 1000 - AUTHENTICATION)", + "CorrelationIdFilter (Priority 100)", + "RequestLoggingFilter (Priority 300)", + "ResponseLoggingFilter (Priority 300)" + ), + "note", "Check server logs for detailed filter execution traces" + )).build(); + + } catch (Exception e) { + LOGGER.severe("Error getting products with filters: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "error", "Error retrieving products", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Gets a specific product using REST client with custom filters. + * + * Demonstrates: + * - Filter execution with path parameters + * - Correlation ID propagation + * - Request/response logging for specific resource retrieval + * - Authentication header injection + * + * Example: GET /catalog/filtered/products/1 + * + * @param id Product ID + * @return Product with comprehensive filter logging + */ + @GET + @Path("/filtered/products/{id}") + @Operation( + summary = "Get product by ID with filters", + description = "Retrieves a specific product using filtered REST client. " + + "Demonstrates filter execution with path parameters and exception handling." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Product retrieved successfully" + ), + @APIResponse( + responseCode = "404", + description = "Product not found" + ) + }) + public Response getProductWithFilters( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("Getting product with custom filters: " + id); + + try { + Product product = filteredCatalogService.getProduct(id); + return Response.ok(Map.of( + "product", product, + "method", "REST Client with Custom Filters", + "filterChain", "BearerToken → CorrelationId → RequestLogging → [HTTP] → ResponseLogging", + "note", "Check server logs for X-Correlation-ID and detailed request/response traces" + )).build(); + + } catch (ProductNotFoundException e) { + LOGGER.warning("Product not found with filters: " + id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of( + "productId", id, + "error", "Product not found", + "note", "404 response was logged by ResponseLoggingFilter before exception was thrown" + )) + .build(); + + } catch (Exception e) { + LOGGER.severe("Error getting product with filters: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Error retrieving product", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Checks product availability using filtered REST client. + * + * Demonstrates: + * - How filters handle error responses (404) + * - Response logging includes error status codes + * - Correlation IDs help trace failed requests + * + * Example: GET /catalog/filtered/products/999999/available + * + * @param id Product ID to check + * @return Availability status with filter logging + */ + @GET + @Path("/filtered/products/{id}/available") + @Operation( + summary = "Check product availability with filters", + description = "Checks if a product is available using filtered REST client. " + + "Demonstrates how filters log both successful and error responses." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Availability check completed" + ) + }) + public Response checkAvailabilityWithFilters( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("Checking availability with filters: " + id); + + try { + boolean available = filteredCatalogService.isProductAvailable(id); + return Response.ok(Map.of( + "productId", id, + "available", available, + "method", "REST Client with Custom Filters", + "note", available + ? "Product found - check logs for filter execution" + : "Product not found - ResponseLoggingFilter logged 404 before returning false" + )).build(); + + } catch (Exception e) { + LOGGER.severe("Error checking availability with filters: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Error checking availability", + "message", e.getMessage() + )) + .build(); + } + } + + /** + * Comparison endpoint showing differences between filtered and non-filtered clients. + * + * Demonstrates: + * - Side-by-side comparison of filtered vs non-filtered REST clients + * - Impact of filters on observability + * - When to use filters vs. plain clients + * + * Example: GET /catalog/compare/1 + * + * @param id Product ID to retrieve with both clients + * @return Comparison results + */ + @GET + @Path("/compare/{id}") + @Operation( + summary = "Compare filtered vs non-filtered clients", + description = "Retrieves the same product using both filtered and non-filtered clients. " + + "Check server logs to compare the logging output." + ) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Comparison completed" + ) + }) + public Response compareClients( + @Parameter(description = "Product ID", required = true) + @PathParam("id") Long id) { + + LOGGER.info("========== Starting Client Comparison =========="); + + try { + // Call with non-filtered client + LOGGER.info("--- Calling NON-FILTERED client ---"); + Product productNoFilter = catalogService.getProduct(id); + + // Small delay for log clarity + Thread.sleep(100); + + // Call with filtered client + LOGGER.info("--- Calling FILTERED client ---"); + Product productWithFilter = filteredCatalogService.getProduct(id); + + LOGGER.info("========== Client Comparison Complete =========="); + + return Response.ok(Map.of( + "productId", id, + "productName", productNoFilter.getName(), + "comparison", Map.of( + "noFilters", "Minimal logging - only business logic logs", + "withFilters", "Comprehensive logging - authentication, tracing, request/response details" + ), + "filterBenefits", List.of( + "Authentication without changing business code", + "Distributed tracing with correlation IDs", + "Complete request/response visibility", + "Easier debugging and monitoring", + "Cross-cutting concerns separated from business logic" + ), + "note", "Review server logs to see the dramatic difference in observability" + )).build(); + + } catch (ProductNotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of( + "productId", id, + "error", "Product not found" + )) + .build(); + + } catch (Exception e) { + LOGGER.severe("Error in comparison: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of( + "productId", id, + "error", "Error during comparison", + "message", e.getMessage() + )) + .build(); + } + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/FilteredProductCatalogService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/FilteredProductCatalogService.java new file mode 100644 index 00000000..6e3ef16a --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/FilteredProductCatalogService.java @@ -0,0 +1,124 @@ +package io.microprofile.tutorial.store.payment.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.microprofile.tutorial.store.payment.client.ProductClientWithFilters; +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Service demonstrating MicroProfile Rest Client with custom filters. + * + * This service injects ProductClientWithFilters which has multiple filters registered: + * - BearerTokenFilter: Adds authentication headers + * - CorrelationIdFilter: Adds distributed tracing headers + * - RequestLoggingFilter: Logs outgoing requests + * - ResponseLoggingFilter: Logs incoming responses + * + * Compare the logs from this service with ProductCatalogService (no filters) + * to see the difference in observability and debugging capabilities. + * + * The filters provide: + * - Authentication without modifying business logic + * - Distributed tracing across microservices + * - Comprehensive request/response logging + * - Header propagation patterns + */ +@ApplicationScoped +public class FilteredProductCatalogService { + + private static final Logger LOGGER = Logger.getLogger(FilteredProductCatalogService.class.getName()); + + @Inject + @RestClient + ProductClientWithFilters filteredClient; + + /** + * Gets all products using filtered REST client. + * + * Watch the logs to see filter execution: + * 1. BearerTokenFilter adds Authorization header + * 2. CorrelationIdFilter adds X-Correlation-ID + * 3. RequestLoggingFilter logs complete request + * 4. HTTP request sent + * 5. HTTP response received + * 6. ResponseLoggingFilter logs complete response + * + * @return List of all products + */ + public List getAllProducts() { + LOGGER.info("Getting all products using filtered REST client"); + + try { + List products = filteredClient.getAllProducts(); + LOGGER.info("Successfully retrieved " + products.size() + " products with filters"); + return products; + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error retrieving products with filters: " + e.getMessage(), e); + throw new RuntimeException("Failed to retrieve products", e); + } + } + + /** + * Gets a specific product by ID using filtered REST client. + * + * Demonstrates filter execution with path parameters. + * Check logs to see how filters handle parameterized requests. + * + * @param productId The product ID + * @return The product + * @throws ProductNotFoundException if product not found + */ + public Product getProduct(Long productId) throws ProductNotFoundException { + LOGGER.info("Getting product with filters: " + productId); + + try { + Product product = filteredClient.getProductById(productId); + LOGGER.info("Successfully retrieved product with filters: " + product.getName()); + return product; + + } catch (ProductNotFoundException e) { + LOGGER.log(Level.WARNING, "Product not found with filters: " + productId, e); + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error retrieving product with filters: " + e.getMessage(), e); + throw new RuntimeException("Failed to retrieve product", e); + } + } + + /** + * Checks if a product is available using filtered REST client. + * + * Demonstrates how filters work with exception handling. + * The 404 response will still be logged by ResponseLoggingFilter + * before the exception is thrown. + * + * @param productId The product ID to check + * @return true if product is available, false otherwise + */ + public boolean isProductAvailable(Long productId) { + LOGGER.info("Checking product availability with filters: " + productId); + + try { + Product product = filteredClient.getProductById(productId); + boolean available = (product != null); + LOGGER.info("Product " + productId + " availability with filters: " + available); + return available; + + } catch (ProductNotFoundException e) { + LOGGER.fine("Product not found with filters (expected for availability check): " + productId); + return false; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error checking availability with filters: " + e.getMessage(), e); + return false; + } + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java new file mode 100644 index 00000000..7e7c6d2d --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/PaymentService.java @@ -0,0 +1,46 @@ +package io.microprofile.tutorial.store.payment.service; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; + + +@RequestScoped +@Path("/authorize") +public class PaymentService { + + @Inject + @ConfigProperty(name = "payment.gateway.endpoint") + private String endpoint; + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Process payment", description = "Process payment using the payment gateway API") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Payment processed successfully"), + @APIResponse(responseCode = "400", description = "Invalid input data"), + @APIResponse(responseCode = "500", description = "Internal server error") + }) + public Response processPayment() { + + // Example logic to call the payment gateway API + System.out.println("Calling payment gateway API at: " + endpoint); + // Assuming a successful payment operation for demonstration purposes + // Actual implementation would involve calling the payment gateway and handling the response + + // Dummy response for successful payment processing + String result = "{\"status\":\"success\", \"message\":\"Payment processed successfully.\"}"; + return Response.ok(result, MediaType.APPLICATION_JSON).build(); + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java new file mode 100644 index 00000000..5c7579c1 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductCatalogService.java @@ -0,0 +1,154 @@ +package io.microprofile.tutorial.store.payment.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.microprofile.tutorial.store.payment.client.ProductClient; +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; +import io.microprofile.tutorial.store.payment.exception.ServiceUnavailableException; + +import java.util.List; +import java.util.logging.Logger; + +/** + * Service demonstrating CDI injection of MicroProfile Rest Client. + * + * This class showcases: + * - @Inject annotation for dependency injection + * - @RestClient qualifier (mandatory in MicroProfile Rest Client 4.0) + * - @ApplicationScoped for efficient singleton pattern + * - Type-safe REST client usage without manual instantiation + * - Automatic configuration via MicroProfile Config + * + * The ProductClient is automatically configured using properties from + * microprofile-config.properties (catalog-service/mp-rest/*) + */ +@ApplicationScoped +public class ProductCatalogService { + + private static final Logger LOGGER = Logger.getLogger(ProductCatalogService.class.getName()); + + /** + * MicroProfile Rest Client injected via CDI. + * The @RestClient qualifier is mandatory in MicroProfile Rest Client 4.0. + */ + @Inject + @RestClient + private ProductClient productClient; + + /** + * Retrieves a product by ID from the catalog service. + * + * Demonstrates: + * - Simple method call on injected REST client + * - Handling checked exception from ResponseExceptionMapper + * - Automatic JSON deserialization to Product object + * - Custom exception handling for 404 responses + * + * @param productId The product ID to retrieve + * @return The product or null if not found + */ + public Product getProduct(Long productId) { + LOGGER.info("Fetching product with ID: " + productId); + try { + Product product = productClient.getProductById(productId); + LOGGER.info("Successfully retrieved product: " + product.getName()); + return product; + } catch (ProductNotFoundException e) { + // Checked exception from ResponseExceptionMapper for 404 responses + LOGGER.warning("Product not found (404): " + e.getMessage()); + return null; + } catch (ServiceUnavailableException e) { + // Unchecked exception from ResponseExceptionMapper for 503 responses + LOGGER.severe("Catalog service unavailable (503): " + e.getMessage()); + throw e; // Re-throw to caller + } catch (RuntimeException e) { + // Other runtime exceptions from ResponseExceptionMapper + LOGGER.severe("Unexpected error: " + e.getMessage()); + throw e; + } + } + + /** + * Checks if a product is available for payment processing. + * + * Demonstrates: + * - Exception handling with REST clients + * - Distinguishing between ProductNotFoundException and other errors + * - Boolean logic based on client response + * + * @param productId The product ID to check + * @return true if product exists and is available, false otherwise + */ + public boolean isProductAvailable(Long productId) { + try { + Product product = productClient.getProductById(productId); + boolean available = product != null && product.getPrice() != null && product.getPrice() > 0; + LOGGER.info("Product " + productId + " availability: " + available); + return available; + } catch (ProductNotFoundException e) { + // Product doesn't exist - return false, don't propagate exception + LOGGER.info("Product " + productId + " not found (404)"); + return false; + } catch (Exception e) { + LOGGER.warning("Error checking product " + productId + " availability: " + e.getMessage()); + return false; + } + } + + /** + * Retrieves all products from the catalog service. + * + * Demonstrates: + * - REST client method returning collections + * - Handling list responses + * + * @return List of all products + */ + public List getAllProducts() { + LOGGER.info("Fetching all products from catalog service"); + try { + List products = productClient.getAllProducts(); + LOGGER.info("Retrieved " + products.size() + " products"); + return products; + } catch (Exception e) { + LOGGER.severe("Failed to retrieve products: " + e.getMessage()); + return List.of(); // Return empty list on error + } + } + + /** + * Validates if a product price matches the expected amount. + * + * Demonstrates: + * - Business logic using REST client data + * - Validation scenarios in payment processing + * - Exception handling for not found products + * + * @param productId The product ID + * @param expectedPrice The expected price + * @return true if prices match, false otherwise + */ + public boolean validateProductPrice(Long productId, Double expectedPrice) { + try { + Product product = productClient.getProductById(productId); + if (product != null && product.getPrice() != null) { + boolean priceMatches = product.getPrice().equals(expectedPrice); + if (!priceMatches) { + LOGGER.warning(String.format( + "Price mismatch for product %d: expected %.2f, actual %.2f", + productId, expectedPrice, product.getPrice() + )); + } + return priceMatches; + } + return false; + } catch (Exception e) { + LOGGER.severe("Failed to validate product price: " + e.getMessage()); + return false; + } + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java new file mode 100644 index 00000000..f0ddb680 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java @@ -0,0 +1,221 @@ +package io.microprofile.tutorial.store.payment.service; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.RestClientBuilder; + +import io.microprofile.tutorial.store.payment.client.ProductClient; +import io.microprofile.tutorial.store.payment.dto.product.Product; +import io.microprofile.tutorial.store.payment.exception.ProductNotFoundException; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Service demonstrating programmatic REST client creation using RestClientBuilder. + * + * This class showcases: + * - Creating REST clients programmatically without CDI injection + * - Using the RestClientBuilder fluent API + * - Try-with-resources pattern for automatic resource cleanup + * - Dynamic configuration from MicroProfile Config + * - Configuring timeouts and connection parameters + * - When to use programmatic vs CDI-based client creation + * + * Use RestClientBuilder when: + * - CDI is unavailable + * - Client configuration must be determined dynamically at runtime + * - Multiple endpoints need different configurations + * - Creating clients in utility methods or batch jobs + * + * Always use try-with-resources to ensure proper resource cleanup. + */ +@ApplicationScoped +public class ProductClientBuilderService { + + private static final Logger LOGGER = Logger.getLogger(ProductClientBuilderService.class.getName()); + + /** + * Checks if a product is available using a programmatically created client. + * Demonstrates the basic RestClientBuilder usage with try-with-resources. + * + * @param productId The product ID to check + * @return true if product exists, false otherwise + */ + public boolean isProductAvailable(Long productId) { + LOGGER.info("Checking product availability using RestClientBuilder: " + productId); + + // MicroProfile Rest Client 4.0 introduces baseUri(String) for convenience + // No need to call URI.create() explicitly + try (ProductClient productClient = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .build(ProductClient.class)) { + + Product product = productClient.getProductById(productId); + LOGGER.info("Product found: " + product.getName()); + return product != null; + + } catch (ProductNotFoundException e) { + LOGGER.log(Level.WARNING, "Product not found with ID: " + productId, e); + return false; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error checking product availability: " + e.getMessage(), e); + return false; + } + } + + /** + * Retrieves all products using a programmatically created client. + * Demonstrates try-with-resources ensuring proper client closure. + * + * @return List of all products, or empty list if error occurs + */ + public List getAllProducts() { + LOGGER.info("Retrieving all products using RestClientBuilder"); + + try (ProductClient productClient = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .build(ProductClient.class)) { + + List products = productClient.getAllProducts(); + LOGGER.info("Retrieved " + products.size() + " products"); + return products; + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error retrieving products: " + e.getMessage(), e); + return List.of(); // Return empty list on error + } + } + + /** + * Creates a REST client with dynamic configuration from MicroProfile Config. + * Demonstrates reading configuration at runtime for flexible deployment. + * + * Configuration properties: + * - catalog-service/mp-rest/url - Base URL of the catalog service + * - catalog-service/mp-rest/connectTimeout - Connection timeout in milliseconds + * - catalog-service/mp-rest/readTimeout - Read timeout in milliseconds + * + * @param productId The product ID to retrieve + * @return The product, or null if not found + */ + public Product getProductWithDynamicConfig(Long productId) { + LOGGER.info("Getting product with dynamic configuration: " + productId); + + Config config = ConfigProvider.getConfig(); + + // Read configuration from MicroProfile Config + String baseUrl = config.getValue("catalog-service/mp-rest/url", String.class); + int connectTimeout = config.getOptionalValue("catalog-service/mp-rest/connectTimeout", Integer.class) + .orElse(3000); + int readTimeout = config.getOptionalValue("catalog-service/mp-rest/readTimeout", Integer.class) + .orElse(5000); + + LOGGER.info(String.format("Using dynamic config - URL: %s, ConnectTimeout: %dms, ReadTimeout: %dms", + baseUrl, connectTimeout, readTimeout)); + + try (ProductClient productClient = RestClientBuilder.newBuilder() + .baseUri(baseUrl) + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(ProductClient.class)) { + + Product product = productClient.getProductById(productId); + LOGGER.info("Product retrieved with dynamic config: " + product.getName()); + return product; + + } catch (ProductNotFoundException e) { + LOGGER.log(Level.WARNING, "Product not found: " + productId, e); + return null; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error retrieving product: " + e.getMessage(), e); + return null; + } + } + + /** + * Creates a REST client with custom configuration based on environment. + * Demonstrates conditional configuration for different deployment scenarios. + * + * @param environment Environment name (e.g., "dev", "staging", "prod") + * @param productId The product ID to retrieve + * @return The product, or null if not found + */ + public Product getProductForEnvironment(String environment, Long productId) { + LOGGER.info("Getting product for environment: " + environment); + + // Determine base URL based on environment + String baseUrl = switch (environment.toLowerCase()) { + case "dev" -> "http://localhost:5050/catalog/api"; + case "staging" -> "http://staging-catalog:8080/catalog/api"; + case "prod" -> "http://catalog-service:8080/catalog/api"; + default -> "http://localhost:5050/catalog/api"; + }; + + // Use shorter timeouts for production + long timeout = "prod".equals(environment) ? 2000 : 5000; + + LOGGER.info(String.format("Using environment config - URL: %s, Timeout: %dms", baseUrl, timeout)); + + try (ProductClient productClient = RestClientBuilder.newBuilder() + .baseUri(baseUrl) + .connectTimeout(timeout, TimeUnit.MILLISECONDS) + .readTimeout(timeout, TimeUnit.MILLISECONDS) + .followRedirects(true) // Enable redirect following + .build(ProductClient.class)) { + + return productClient.getProductById(productId); + + } catch (ProductNotFoundException e) { + LOGGER.log(Level.WARNING, "Product not found: " + productId, e); + return null; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error retrieving product: " + e.getMessage(), e); + return null; + } + } + + /** + * Demonstrates creating multiple clients with different configurations. + * Shows when programmatic creation is preferred over CDI injection. + * + * @param productIds List of product IDs to check + * @return Number of available products + */ + public int checkMultipleProducts(List productIds) { + LOGGER.info("Checking availability for " + productIds.size() + " products"); + + int availableCount = 0; + + for (Long productId : productIds) { + // Create a new client for each product check + // This demonstrates the flexibility of programmatic creation + try (ProductClient productClient = RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(1, TimeUnit.SECONDS) // Shorter timeout for batch operations + .readTimeout(2, TimeUnit.SECONDS) + .build(ProductClient.class)) { + + productClient.getProductById(productId); + availableCount++; + + } catch (ProductNotFoundException e) { + LOGGER.fine("Product not found: " + productId); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error checking product: " + productId, e); + } + } + + LOGGER.info(String.format("Found %d out of %d products available", + availableCount, productIds.size())); + + return availableCount; + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java new file mode 100644 index 00000000..ba677ea3 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/ProductIntegrationService.java @@ -0,0 +1,148 @@ +package io.microprofile.tutorial.store.payment.service; + +import io.microprofile.tutorial.store.payment.client.ProductJakartaRestClient; +import io.microprofile.tutorial.store.payment.dto.product.Product; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Service for integrating with the Product/Catalog service. + * Provides business logic for product validation and retrieval in the context of payment processing. + */ +@ApplicationScoped +public class ProductIntegrationService { + + private static final Logger LOGGER = Logger.getLogger(ProductIntegrationService.class.getName()); + + @Inject + @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog/api/products") + private String catalogServiceUrl; + + /** + * Validates if a product is suitable for payment processing. + * + * @param productId The product ID to validate + * @return true if the product is valid for payment, false otherwise + */ + public boolean validateProductForPayment(Long productId) { + LOGGER.info("Validating product for payment: " + productId); + + try { + Product product = getProductDetails(productId); + if (product == null) { + LOGGER.warning("Product not found: " + productId); + return false; + } + + // Basic validation: product must have a valid price and name + boolean isValid = product.price != null && + product.price > 0.0 && + product.name != null && + !product.name.trim().isEmpty(); + + LOGGER.info("Product " + productId + " validation result: " + isValid); + return isValid; + + } catch (Exception e) { + LOGGER.severe("Error validating product " + productId + ": " + e.getMessage()); + return false; + } + } + + /** + * Gets detailed information about a specific product. + * + * @param productId The product ID + * @return Product details or null if not found + */ + public Product getProductDetails(Long productId) { + LOGGER.info("Fetching product details for ID: " + productId); + + try { + Product[] allProducts = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); + + if (allProducts != null) { + for (Product product : allProducts) { + if (product.id.equals(productId)) { + LOGGER.info("Found product: " + product.name + " (ID: " + productId + ")"); + return product; + } + } + } + + LOGGER.warning("Product not found with ID: " + productId); + return null; + + } catch (Exception e) { + LOGGER.severe("Error fetching product details for ID " + productId + ": " + e.getMessage()); + return null; + } + } + + /** + * Gets products within a specified price range. + * + * @param minPrice Minimum price (inclusive) + * @param maxPrice Maximum price (inclusive) + * @return List of products within the price range + */ + public List getProductsByPriceRange(double minPrice, double maxPrice) { + LOGGER.info("Fetching products in price range: " + minPrice + " - " + maxPrice); + + try { + Product[] allProducts = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); + + if (allProducts == null) { + LOGGER.warning("No products returned from catalog service"); + return new ArrayList<>(); + } + + List filteredProducts = Arrays.stream(allProducts) + .filter(product -> product.price != null) + .filter(product -> product.price >= minPrice) + .filter(product -> product.price <= maxPrice) + .collect(Collectors.toList()); + + LOGGER.info("Found " + filteredProducts.size() + " products in price range"); + return filteredProducts; + + } catch (Exception e) { + LOGGER.severe("Error fetching products by price range: " + e.getMessage()); + return new ArrayList<>(); + } + } + + /** + * Gets all available products. + * + * @return Array of all products + */ + public Product[] getAllProducts() { + LOGGER.info("Fetching all products"); + + try { + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); + + if (products != null) { + LOGGER.info("Retrieved " + products.length + " products"); + } else { + LOGGER.warning("No products returned from catalog service"); + products = new Product[0]; + } + + return products; + + } catch (Exception e) { + LOGGER.severe("Error fetching all products: " + e.getMessage()); + return new Product[0]; + } + } +} diff --git a/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http new file mode 100644 index 00000000..98ae2e52 --- /dev/null +++ b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/service/payment.http @@ -0,0 +1,9 @@ +POST https://orange-zebra-r745vp6rjcxp67-9080.app.github.dev/payment/authorize + +{ + "cardNumber": "4111111111111111", + "cardHolderName": "John Doe", + "expiryDate": "12/25", + "securityCode": "123", + "amount": 100.00 +} \ No newline at end of file diff --git a/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..93c66f76 --- /dev/null +++ b/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,35 @@ +# microprofile-config.properties +mp.openapi.scan.disable=false +product.maintenanceMode=false + +# Product Service Configuration +payment.gateway.endpoint=https://api.paymentgateway.com/v1 + +# Catalog Service Configuration for ProductClientJson +catalog.service.url=http://localhost:5050/catalog/api/products +catalog.service.fallback.url=http://localhost:6050/products +catalog.service.timeout=5000 + +# MicroProfile Rest Client Configuration for ProductClient (catalog-service) +# Base URL - required property for REST client +catalog-service/mp-rest/url=http://localhost:5050/catalog/api +# Connection timeout in milliseconds +catalog-service/mp-rest/connectTimeout=3000 +# Read timeout in milliseconds +catalog-service/mp-rest/readTimeout=5000 +# CDI scope for the injected client +catalog-service/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped +# Follow HTTP redirects +catalog-service/mp-rest/followRedirects=true + +# MicroProfile Rest Client Configuration for ProductClientWithFilters (catalog-service-filtered) +# This client demonstrates custom filters and interceptors +catalog-service-filtered/mp-rest/url=http://localhost:5050/catalog/api +catalog-service-filtered/mp-rest/connectTimeout=3000 +catalog-service-filtered/mp-rest/readTimeout=5000 +catalog-service-filtered/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped +catalog-service-filtered/mp-rest/followRedirects=true + +# Optional Bearer token for authentication (used by BearerTokenFilter) +# Uncomment and set your token to enable authentication +# catalog-service.bearer.token=your-secret-bearer-token-here diff --git a/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource new file mode 100644 index 00000000..98707178 --- /dev/null +++ b/code/chapter11/payment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource @@ -0,0 +1 @@ +io.microprofile.tutorial.store.payment.config.PaymentServiceConfigSource \ No newline at end of file diff --git a/code/chapter11/payment/src/main/webapp/WEB-INF/beans.xml b/code/chapter11/payment/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 00000000..8b7e8292 --- /dev/null +++ b/code/chapter11/payment/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,16 @@ + + + + diff --git a/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..9e4411be --- /dev/null +++ b/code/chapter11/payment/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + Payment Service + + + index.html + index.jsp + + diff --git a/code/chapter11/payment/src/main/webapp/index.html b/code/chapter11/payment/src/main/webapp/index.html new file mode 100644 index 00000000..33086f26 --- /dev/null +++ b/code/chapter11/payment/src/main/webapp/index.html @@ -0,0 +1,140 @@ + + + + + + Payment Service - MicroProfile Config Demo + + + +
+

Payment Service

+

MicroProfile Config Demo with Custom ConfigSource

+
+ +
+
+

About this Service

+

The Payment Service demonstrates MicroProfile Config integration with custom ConfigSource implementation.

+

It provides endpoints for managing payment configuration and processing payments using dynamic configuration.

+

Key Features:

+
    +
  • Custom MicroProfile ConfigSource with ordinal 600 (highest priority)
  • +
  • Dynamic configuration updates via REST API
  • +
  • Payment gateway endpoint configuration
  • +
  • Real-time configuration access for payment processing
  • +
+
+ +
+

API Endpoints

+
    +
  • GET /api/payment-config - Get current payment configuration
  • +
  • POST /api/payment-config - Update payment configuration property
  • +
  • POST /api/authorize - Process payment authorization
  • +
  • POST /api/payment-config/process-example - Example payment processing with config
  • +
+
+ +
+

Configuration Management

+

This service implements a custom MicroProfile ConfigSource that allows dynamic configuration updates:

+
    +
  • Configuration Priority: Custom ConfigSource (600) > System Properties (400) > Environment Variables (300) > microprofile-config.properties (100)
  • +
  • Current Properties: payment.gateway.endpoint
  • +
  • Update Method: POST to /api/payment-config with {"key": "payment.property.name", "value": "new-value"}
  • +
+
+ + +
+ +
+

MicroProfile Config Demo | Payment Service

+

Powered by Open Liberty & MicroProfile Config 3.0

+
+ + diff --git a/code/chapter11/payment/src/main/webapp/index.jsp b/code/chapter11/payment/src/main/webapp/index.jsp new file mode 100644 index 00000000..d5de5cb2 --- /dev/null +++ b/code/chapter11/payment/src/main/webapp/index.jsp @@ -0,0 +1,12 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> + + + + + + Redirecting... + + +

Redirecting to the Payment Service homepage...

+ + diff --git a/code/chapter11/run-all-services.sh b/code/chapter11/run-all-services.sh new file mode 100644 index 00000000..7a63b901 --- /dev/null +++ b/code/chapter11/run-all-services.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Build all projects +echo "Building User Service..." +cd user && mvn clean package && cd .. + +echo "Building Inventory Service..." +cd inventory && mvn clean package && cd .. + +echo "Building Order Service..." +cd order && mvn clean package && cd .. + +echo "Building Catalog Service..." +cd catalog && mvn clean package && cd .. + +echo "Building Payment Service..." +cd payment && mvn clean package && cd .. + +echo "Building Shopping Cart Service..." +cd shoppingcart && mvn clean package && cd .. + +echo "Building Shipment Service..." +cd shipment && mvn clean package && cd .. + +# Start all services using docker-compose +echo "Starting all services with Docker Compose..." +docker-compose up -d + +echo "All services are running:" +echo "- User Service: https:///user" +echo "- Inventory Service: https:///inventory" +echo "- Order Service: https:///order" +echo "- Catalog Service: https:///catalog" +echo "- Payment Service: https:///payment" +echo "- Shopping Cart Service: https:///shoppingcart" +echo "- Shipment Service: https:///shipment" \ No newline at end of file diff --git a/code/chapter11/shipment/Dockerfile b/code/chapter11/shipment/Dockerfile new file mode 100644 index 00000000..287b43d0 --- /dev/null +++ b/code/chapter11/shipment/Dockerfile @@ -0,0 +1,27 @@ +FROM icr.io/appcafe/open-liberty:23.0.0.3-full-java17-openj9-ubi + +# Copy config +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Create the app directory +COPY --chown=1001:0 target/shipment.war /config/apps/ + +# Optional: Copy utility scripts +COPY --chown=1001:0 *.sh /opt/ol/helpers/ + +# Environment variables +ENV VERBOSE=true + +# This is important - adds the management of vulnerability databases to allow Docker scanning +RUN dnf install -y shadow-utils + +# Set environment variable for MP config profile +ENV MP_CONFIG_PROFILE=docker + +EXPOSE 8060 9060 + +# Run as non-root user for security +USER 1001 + +# Start Liberty +ENTRYPOINT ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/shipment/README.md b/code/chapter11/shipment/README.md new file mode 100644 index 00000000..41619949 --- /dev/null +++ b/code/chapter11/shipment/README.md @@ -0,0 +1,87 @@ +# Shipment Service + +This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. + +## Overview + +The Shipment Service is responsible for: +- Creating shipments for orders +- Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) +- Assigning tracking numbers +- Estimating delivery dates +- Communicating with the Order Service to update order status + +## Technologies + +The Shipment Service is built using: +- Jakarta EE 10 +- MicroProfile 6.1 +- Open Liberty +- Java 17 + +## Getting Started + +### Prerequisites + +- JDK 17+ +- Maven 3.8+ +- Docker (for containerized deployment) + +### Running Locally + +To build and run the service: + +```bash +./run.sh +``` + +This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment + +### Running with Docker + +To build and run the service in a Docker container: + +```bash +./run-docker.sh +``` + +This will build a Docker image for the service and run it, exposing ports 8060 and 9060. + +## API Endpoints + +| Method | URL | Description | +|--------|-------------------------------------------|--------------------------------------| +| POST | /api/shipments/orders/{orderId} | Create a new shipment | +| GET | /api/shipments/{shipmentId} | Get a shipment by ID | +| GET | /api/shipments | Get all shipments | +| GET | /api/shipments/status/{status} | Get shipments by status | +| GET | /api/shipments/orders/{orderId} | Get shipments for an order | +| GET | /api/shipments/tracking/{trackingNumber} | Get a shipment by tracking number | +| PUT | /api/shipments/{shipmentId}/status/{status} | Update shipment status | +| PUT | /api/shipments/{shipmentId}/carrier | Update shipment carrier | +| PUT | /api/shipments/{shipmentId}/tracking | Update shipment tracking number | +| PUT | /api/shipments/{shipmentId}/delivery-date | Update estimated delivery date | +| PUT | /api/shipments/{shipmentId}/notes | Update shipment notes | +| DELETE | /api/shipments/{shipmentId} | Delete a shipment | + +## MicroProfile Features + +The service utilizes several MicroProfile features: + +- **Config**: For external configuration +- **Health**: For liveness and readiness checks +- **Metrics**: For monitoring service performance +- **Fault Tolerance**: For resilient communication with the Order Service +- **OpenAPI**: For API documentation + +## Documentation + +API documentation is available at: +- OpenAPI: http://localhost:8060/shipment/openapi +- Swagger UI: http://localhost:8060/shipment/openapi/ui + +## Monitoring + +Health and metrics endpoints: +- Health: http://localhost:8060/shipment/health +- Metrics: http://localhost:8060/shipment/metrics diff --git a/code/chapter11/shipment/pom.xml b/code/chapter11/shipment/pom.xml new file mode 100644 index 00000000..d5e31f65 --- /dev/null +++ b/code/chapter11/shipment/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.microprofile + shipment + 1.0-SNAPSHOT + war + + shipment-service + https://microprofile.io + + + UTF-8 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + shipment + + + + io.openliberty.tools + liberty-maven-plugin + 3.8.2 + + shipmentServer + runnable + 120 + + /shipment + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/shipment/run-docker.sh b/code/chapter11/shipment/run-docker.sh new file mode 100644 index 00000000..69a51506 --- /dev/null +++ b/code/chapter11/shipment/run-docker.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Build and run the Shipment Service in Docker +echo "Building and starting Shipment Service in Docker..." + +# Build the application +mvn clean package + +# Build and run the Docker image +docker build -t shipment-service . +docker run -p 8060:8060 -p 9060:9060 --name shipment-service shipment-service diff --git a/code/chapter11/shipment/run.sh b/code/chapter11/shipment/run.sh new file mode 100644 index 00000000..b6fd34ac --- /dev/null +++ b/code/chapter11/shipment/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Build and run the Shipment Service +echo "Building and starting Shipment Service..." + +# Stop running server if already running +if [ -f target/liberty/wlp/usr/servers/shipmentServer/workarea/.sRunning ]; then + mvn liberty:stop +fi + +# Clean, build and run +mvn clean package liberty:run diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java new file mode 100644 index 00000000..3f7288ba --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/ShipmentApplication.java @@ -0,0 +1,35 @@ +package io.microprofile.tutorial.store.shipment; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.info.Contact; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.License; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * JAX-RS Application class for the shipment service. + */ +@ApplicationPath("/api") +@OpenAPIDefinition( + info = @Info( + title = "Shipment Service API", + version = "1.0.0", + description = "API for managing shipments in the microprofile tutorial store", + contact = @Contact( + name = "Shipment Service Support", + email = "shipment@example.com" + ), + license = @License( + name = "Apache 2.0", + url = "https://www.apache.org/licenses/LICENSE-2.0.html" + ) + ), + tags = { + @Tag(name = "Shipment Resource", description = "Operations for managing shipments") + } +) +public class ShipmentApplication extends Application { + // Empty application class, all configuration is provided by annotations +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java new file mode 100644 index 00000000..ba3ce1f8 --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/client/OrderClient.java @@ -0,0 +1,193 @@ +package io.microprofile.tutorial.store.shipment.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Order Service. + */ +@ApplicationScoped +public class OrderClient { + + private static final Logger LOGGER = Logger.getLogger(OrderClient.class.getName()); + + @ConfigProperty(name = "order.service.url", defaultValue = "http://localhost:8050/order") + private String orderServiceUrl; + + /** + * Updates the order status after a shipment has been processed. + * + * @param orderId The ID of the order to update + * @param newStatus The new status for the order + * @return true if the update was successful, false otherwise + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "updateOrderStatusFallback") + public boolean updateOrderStatus(Long orderId, String newStatus) { + LOGGER.info(String.format("Updating order %d status to %s", orderId, newStatus)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d/status/%s", orderServiceUrl, orderId, newStatus); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .put(Entity.json("{}")); + + boolean success = response.getStatus() == Response.Status.OK.getStatusCode(); + if (!success) { + LOGGER.warning(String.format("Failed to update order status. Status code: %d", response.getStatus())); + } + return success; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Verifies that an order exists and is in a valid state for shipment. + * + * @param orderId The ID of the order to verify + * @return true if the order exists and is in a valid state, false otherwise + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "verifyOrderFallback") + public boolean verifyOrder(Long orderId) { + LOGGER.info(String.format("Verifying order %d for shipment", orderId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple check if the order is in a valid state for shipment + // In a real app, we'd parse the JSON properly + return jsonResponse.contains("\"status\":\"PAID\"") || + jsonResponse.contains("\"status\":\"PROCESSING\"") || + jsonResponse.contains("\"status\":\"READY_FOR_SHIPMENT\""); + } + + LOGGER.warning(String.format("Failed to verify order. Status code: %d", response.getStatus())); + return false; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Gets the shipping address for an order. + * + * @param orderId The ID of the order + * @return The shipping address, or null if not found + */ + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "getShippingAddressFallback") + public String getShippingAddress(Long orderId) { + LOGGER.info(String.format("Getting shipping address for order %d", orderId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/orders/%d", orderServiceUrl, orderId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple extract of shipping address - in real app use proper JSON parsing + if (jsonResponse.contains("\"shippingAddress\":")) { + int startIndex = jsonResponse.indexOf("\"shippingAddress\":") + "\"shippingAddress\":".length(); + startIndex = jsonResponse.indexOf("\"", startIndex) + 1; + int endIndex = jsonResponse.indexOf("\"", startIndex); + if (endIndex > startIndex) { + return jsonResponse.substring(startIndex, endIndex); + } + } + } + + LOGGER.warning(String.format("Failed to get shipping address. Status code: %d", response.getStatus())); + return null; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Order Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for updateOrderStatus. + * + * @param orderId The ID of the order + * @param newStatus The new status for the order + * @return false, indicating failure + */ + public boolean updateOrderStatusFallback(Long orderId, String newStatus) { + LOGGER.warning(String.format("Using fallback for order status update. Order ID: %d, Status: %s", orderId, newStatus)); + return false; + } + + /** + * Fallback method for verifyOrder. + * + * @param orderId The ID of the order + * @return false, indicating failure + */ + public boolean verifyOrderFallback(Long orderId) { + LOGGER.warning(String.format("Using fallback for order verification. Order ID: %d", orderId)); + return false; + } + + /** + * Fallback method for getShippingAddress. + * + * @param orderId The ID of the order + * @return null, indicating failure + */ + public String getShippingAddressFallback(Long orderId) { + LOGGER.warning(String.format("Using fallback for getting shipping address. Order ID: %d", orderId)); + return null; + } +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java new file mode 100644 index 00000000..d9bea89e --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/Shipment.java @@ -0,0 +1,45 @@ +package io.microprofile.tutorial.store.shipment.entity; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Shipment class for the microprofile tutorial store application. + * This class represents a shipment of an order in the system. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Shipment { + + private Long shipmentId; + + @NotNull(message = "Order ID cannot be null") + private Long orderId; + + private String trackingNumber; + + @NotNull(message = "Status cannot be null") + private ShipmentStatus status; + + private LocalDateTime estimatedDelivery; + + private LocalDateTime shippedAt; + + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + private LocalDateTime updatedAt; + + private String carrier; + + private String shippingAddress; + + private String notes; +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java new file mode 100644 index 00000000..0e120a9b --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/entity/ShipmentStatus.java @@ -0,0 +1,16 @@ +package io.microprofile.tutorial.store.shipment.entity; + +/** + * ShipmentStatus enum for the microprofile tutorial store application. + * This enum defines the possible statuses for a shipment. + */ +public enum ShipmentStatus { + PENDING, // Shipment is pending + PROCESSING, // Shipment is being processed + SHIPPED, // Shipment has been shipped + IN_TRANSIT, // Shipment is in transit + OUT_FOR_DELIVERY,// Shipment is out for delivery + DELIVERED, // Shipment has been delivered + FAILED, // Shipment delivery failed + RETURNED // Shipment was returned +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java new file mode 100644 index 00000000..ec26495e --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/filter/CorsFilter.java @@ -0,0 +1,43 @@ +package io.microprofile.tutorial.store.shipment.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Filter to enable CORS for the Shipment service. + */ +public class CorsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // No initialization required + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Allow requests from any origin + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + response.setHeader("Access-Control-Max-Age", "3600"); + + // For preflight requests + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // No cleanup required + } +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java new file mode 100644 index 00000000..4bf8a503 --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/health/ShipmentHealthCheck.java @@ -0,0 +1,67 @@ +package io.microprofile.tutorial.store.shipment.health; + +import io.microprofile.tutorial.store.shipment.client.OrderClient; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.eclipse.microprofile.health.Readiness; + +/** + * Health check for the shipment service. + */ +@ApplicationScoped +public class ShipmentHealthCheck { + + @Inject + private OrderClient orderClient; + + /** + * Liveness check for the shipment service. + * Verifies that the application is running and not in a failed state. + * + * @return HealthCheckResponse indicating whether the service is live + */ + @Liveness + @ApplicationScoped + public static class LivenessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.named("shipment-liveness") + .up() + .withData("memory", Runtime.getRuntime().freeMemory()) + .build(); + } + } + + /** + * Readiness check for the shipment service. + * Verifies that the service is ready to handle requests, including connectivity to dependencies. + * + * @return HealthCheckResponse indicating whether the service is ready + */ + @Readiness + @ApplicationScoped + public class ReadinessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + boolean orderServiceReachable = false; + + try { + // Simple check to see if the Order service is reachable + // We use a dummy order ID just to test connectivity + orderClient.getShippingAddress(999999L); + orderServiceReachable = true; + } catch (Exception e) { + // If the order service is not reachable, the health check will fail + orderServiceReachable = false; + } + + return HealthCheckResponse.named("shipment-readiness") + .status(orderServiceReachable) + .withData("orderServiceReachable", orderServiceReachable) + .build(); + } + } +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java new file mode 100644 index 00000000..c4013a92 --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/repository/ShipmentRepository.java @@ -0,0 +1,148 @@ +package io.microprofile.tutorial.store.shipment.repository; + +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Simple in-memory repository for Shipment objects. + * This class provides CRUD operations for Shipment entities. + */ +@ApplicationScoped +public class ShipmentRepository { + + private final Map shipments = new ConcurrentHashMap<>(); + private long nextId = 1; + + /** + * Saves a shipment to the repository. + * If the shipment has no ID, a new ID is assigned. + * + * @param shipment The shipment to save + * @return The saved shipment with ID assigned + */ + public Shipment save(Shipment shipment) { + if (shipment.getShipmentId() == null) { + shipment.setShipmentId(nextId++); + } + + if (shipment.getCreatedAt() == null) { + shipment.setCreatedAt(LocalDateTime.now()); + } + + shipment.setUpdatedAt(LocalDateTime.now()); + + shipments.put(shipment.getShipmentId(), shipment); + return shipment; + } + + /** + * Finds a shipment by ID. + * + * @param id The shipment ID + * @return An Optional containing the shipment if found, or empty if not found + */ + public Optional findById(Long id) { + return Optional.ofNullable(shipments.get(id)); + } + + /** + * Finds shipments by order ID. + * + * @param orderId The order ID + * @return A list of shipments for the specified order + */ + public List findByOrderId(Long orderId) { + return shipments.values().stream() + .filter(shipment -> shipment.getOrderId().equals(orderId)) + .collect(Collectors.toList()); + } + + /** + * Finds shipments by tracking number. + * + * @param trackingNumber The tracking number + * @return A list of shipments with the specified tracking number + */ + public List findByTrackingNumber(String trackingNumber) { + return shipments.values().stream() + .filter(shipment -> trackingNumber.equals(shipment.getTrackingNumber())) + .collect(Collectors.toList()); + } + + /** + * Finds shipments by status. + * + * @param status The shipment status + * @return A list of shipments with the specified status + */ + public List findByStatus(ShipmentStatus status) { + return shipments.values().stream() + .filter(shipment -> shipment.getStatus() == status) + .collect(Collectors.toList()); + } + + /** + * Finds shipments that are expected to be delivered by a certain date. + * + * @param deliveryDate The delivery date + * @return A list of shipments expected to be delivered by the specified date + */ + public List findByEstimatedDeliveryBefore(LocalDateTime deliveryDate) { + return shipments.values().stream() + .filter(shipment -> shipment.getEstimatedDelivery() != null && + shipment.getEstimatedDelivery().isBefore(deliveryDate)) + .collect(Collectors.toList()); + } + + /** + * Retrieves all shipments from the repository. + * + * @return A list of all shipments + */ + public List findAll() { + return new ArrayList<>(shipments.values()); + } + + /** + * Deletes a shipment by ID. + * + * @param id The ID of the shipment to delete + * @return true if the shipment was deleted, false if not found + */ + public boolean deleteById(Long id) { + return shipments.remove(id) != null; + } + + /** + * Updates an existing shipment. + * + * @param id The ID of the shipment to update + * @param shipment The updated shipment information + * @return An Optional containing the updated shipment, or empty if not found + */ + public Optional update(Long id, Shipment shipment) { + if (!shipments.containsKey(id)) { + return Optional.empty(); + } + + // Preserve creation date + LocalDateTime createdAt = shipments.get(id).getCreatedAt(); + shipment.setCreatedAt(createdAt); + + shipment.setShipmentId(id); + shipment.setUpdatedAt(LocalDateTime.now()); + + shipments.put(id, shipment); + return Optional.of(shipment); + } +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java new file mode 100644 index 00000000..602be808 --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java @@ -0,0 +1,397 @@ +package io.microprofile.tutorial.store.shipment.resource; + +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; +import io.microprofile.tutorial.store.shipment.service.ShipmentService; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * REST resource for shipment operations. + */ +@Path("/api/shipments") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Shipment Resource", description = "Operations for managing shipments") +public class ShipmentResource { + + private static final Logger LOGGER = Logger.getLogger(ShipmentResource.class.getName()); + + @Inject + private ShipmentService shipmentService; + + /** + * Creates a new shipment for an order. + * + * @param orderId The order ID + * @return The created shipment + */ + @POST + @Path("/orders/{orderId}") + @Operation(summary = "Create a new shipment for an order") + @APIResponse(responseCode = "201", description = "Shipment created", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "400", description = "Invalid order ID") + @APIResponse(responseCode = "404", description = "Order not found or not ready for shipment") + public Response createShipment( + @Parameter(description = "Order ID", required = true) + @PathParam("orderId") Long orderId) { + + LOGGER.info("REST request to create shipment for order: " + orderId); + + Shipment shipment = shipmentService.createShipment(orderId); + if (shipment == null) { + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Order not found or not ready for shipment\"}") + .build(); + } + + return Response.status(Response.Status.CREATED) + .entity(shipment) + .build(); + } + + /** + * Gets a shipment by ID. + * + * @param shipmentId The shipment ID + * @return The shipment + */ + @GET + @Path("/{shipmentId}") + @Operation(summary = "Get a shipment by ID") + @APIResponse(responseCode = "200", description = "Shipment found", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response getShipment( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId) { + + LOGGER.info("REST request to get shipment: " + shipmentId); + + Optional shipment = shipmentService.getShipment(shipmentId); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Gets all shipments. + * + * @return All shipments + */ + @GET + @Operation(summary = "Get all shipments") + @APIResponse(responseCode = "200", description = "All shipments", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getAllShipments() { + LOGGER.info("REST request to get all shipments"); + + List shipments = shipmentService.getAllShipments(); + return Response.ok(shipments).build(); + } + + /** + * Gets shipments by status. + * + * @param status The status + * @return The shipments with the given status + */ + @GET + @Path("/status/{status}") + @Operation(summary = "Get shipments by status") + @APIResponse(responseCode = "200", description = "Shipments with the given status", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getShipmentsByStatus( + @Parameter(description = "Shipment status", required = true) + @PathParam("status") ShipmentStatus status) { + + LOGGER.info("REST request to get shipments with status: " + status); + + List shipments = shipmentService.getShipmentsByStatus(status); + return Response.ok(shipments).build(); + } + + /** + * Gets shipments by order ID. + * + * @param orderId The order ID + * @return The shipments for the given order + */ + @GET + @Path("/orders/{orderId}") + @Operation(summary = "Get shipments by order ID") + @APIResponse(responseCode = "200", description = "Shipments for the given order", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Shipment.class))) + public Response getShipmentsByOrder( + @Parameter(description = "Order ID", required = true) + @PathParam("orderId") Long orderId) { + + LOGGER.info("REST request to get shipments for order: " + orderId); + + List shipments = shipmentService.getShipmentsByOrder(orderId); + return Response.ok(shipments).build(); + } + + /** + * Gets a shipment by tracking number. + * + * @param trackingNumber The tracking number + * @return The shipment + */ + @GET + @Path("/tracking/{trackingNumber}") + @Operation(summary = "Get a shipment by tracking number") + @APIResponse(responseCode = "200", description = "Shipment found", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response getShipmentByTrackingNumber( + @Parameter(description = "Tracking number", required = true) + @PathParam("trackingNumber") String trackingNumber) { + + LOGGER.info("REST request to get shipment with tracking number: " + trackingNumber); + + Optional shipment = shipmentService.getShipmentByTrackingNumber(trackingNumber); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the status of a shipment. + * + * @param shipmentId The shipment ID + * @param status The new status + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/status/{status}") + @Operation(summary = "Update shipment status") + @APIResponse(responseCode = "200", description = "Shipment status updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateShipmentStatus( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New status", required = true) + @PathParam("status") ShipmentStatus status) { + + LOGGER.info("REST request to update shipment " + shipmentId + " status to " + status); + + Optional shipment = shipmentService.updateShipmentStatus(shipmentId, status); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the carrier for a shipment. + * + * @param shipmentId The shipment ID + * @param carrier The new carrier + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/carrier") + @Operation(summary = "Update shipment carrier") + @APIResponse(responseCode = "200", description = "Carrier updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateCarrier( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New carrier", required = true) + @NotNull String carrier) { + + LOGGER.info("REST request to update carrier for shipment " + shipmentId + " to " + carrier); + + Optional shipment = shipmentService.updateCarrier(shipmentId, carrier); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the tracking number for a shipment. + * + * @param shipmentId The shipment ID + * @param trackingNumber The new tracking number + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/tracking") + @Operation(summary = "Update shipment tracking number") + @APIResponse(responseCode = "200", description = "Tracking number updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateTrackingNumber( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New tracking number", required = true) + @NotNull String trackingNumber) { + + LOGGER.info("REST request to update tracking number for shipment " + shipmentId + " to " + trackingNumber); + + Optional shipment = shipmentService.updateTrackingNumber(shipmentId, trackingNumber); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Updates the estimated delivery date for a shipment. + * + * @param shipmentId The shipment ID + * @param dateStr The new estimated delivery date (ISO format) + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/delivery-date") + @Operation(summary = "Update shipment estimated delivery date") + @APIResponse(responseCode = "200", description = "Estimated delivery date updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "400", description = "Invalid date format") + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateEstimatedDelivery( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New estimated delivery date (ISO format: yyyy-MM-dd'T'HH:mm:ss)", required = true) + @NotNull String dateStr) { + + LOGGER.info("REST request to update estimated delivery for shipment " + shipmentId + " to " + dateStr); + + try { + LocalDateTime date = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + Optional shipment = shipmentService.updateEstimatedDelivery(shipmentId, date); + + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Invalid date format. Use ISO format: yyyy-MM-dd'T'HH:mm:ss\"}") + .build(); + } + } + + /** + * Updates the notes for a shipment. + * + * @param shipmentId The shipment ID + * @param notes The new notes + * @return The updated shipment + */ + @PUT + @Path("/{shipmentId}/notes") + @Operation(summary = "Update shipment notes") + @APIResponse(responseCode = "200", description = "Notes updated", + content = @Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = Shipment.class))) + @APIResponse(responseCode = "404", description = "Shipment not found") + public Response updateNotes( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId, + @Parameter(description = "New notes", required = true) + String notes) { + + LOGGER.info("REST request to update notes for shipment " + shipmentId); + + Optional shipment = shipmentService.updateNotes(shipmentId, notes); + if (shipment.isPresent()) { + return Response.ok(shipment.get()).build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } + + /** + * Deletes a shipment. + * + * @param shipmentId The shipment ID + * @return A response indicating success or failure + */ + @DELETE + @Path("/{shipmentId}") + @Operation(summary = "Delete a shipment") + @APIResponse(responseCode = "204", description = "Shipment deleted") + @APIResponse(responseCode = "404", description = "Shipment not found") + @APIResponse(responseCode = "400", description = "Shipment cannot be deleted due to its status") + public Response deleteShipment( + @Parameter(description = "Shipment ID", required = true) + @PathParam("shipmentId") Long shipmentId) { + + LOGGER.info("REST request to delete shipment: " + shipmentId); + + boolean deleted = shipmentService.deleteShipment(shipmentId); + if (deleted) { + return Response.noContent().build(); + } + + // Check if shipment exists but cannot be deleted due to its status + Optional shipment = shipmentService.getShipment(shipmentId); + if (shipment.isPresent()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Shipment cannot be deleted due to its status\"}") + .build(); + } + + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"error\": \"Shipment not found\"}") + .build(); + } +} diff --git a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java new file mode 100644 index 00000000..f29aadee --- /dev/null +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/service/ShipmentService.java @@ -0,0 +1,305 @@ +package io.microprofile.tutorial.store.shipment.service; + +import io.microprofile.tutorial.store.shipment.client.OrderClient; +import io.microprofile.tutorial.store.shipment.entity.Shipment; +import io.microprofile.tutorial.store.shipment.entity.ShipmentStatus; +import io.microprofile.tutorial.store.shipment.repository.ShipmentRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.Timed; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.logging.Logger; + +/** + * Shipment Service for managing shipments. + */ +@ApplicationScoped +public class ShipmentService { + + private static final Logger LOGGER = Logger.getLogger(ShipmentService.class.getName()); + private static final Random RANDOM = new Random(); + private static final String[] CARRIERS = {"FedEx", "UPS", "USPS", "DHL", "Amazon Logistics"}; + + @Inject + private ShipmentRepository shipmentRepository; + + @Inject + private OrderClient orderClient; + + /** + * Creates a new shipment for an order. + * + * @param orderId The order ID + * @return The created shipment, or null if the order is invalid + */ + @Counted(name = "shipmentCreations", description = "Number of shipments created") + @Timed(name = "createShipmentTimer", description = "Time to create a shipment") + public Shipment createShipment(Long orderId) { + LOGGER.info("Creating shipment for order: " + orderId); + + // Verify that the order exists and is ready for shipment + if (!orderClient.verifyOrder(orderId)) { + LOGGER.warning("Order " + orderId + " is not valid for shipment"); + return null; + } + + // Get shipping address from order service + String shippingAddress = orderClient.getShippingAddress(orderId); + if (shippingAddress == null) { + LOGGER.warning("Could not retrieve shipping address for order " + orderId); + return null; + } + + // Create a new shipment + Shipment shipment = Shipment.builder() + .orderId(orderId) + .status(ShipmentStatus.PENDING) + .trackingNumber(generateTrackingNumber()) + .carrier(selectRandomCarrier()) + .shippingAddress(shippingAddress) + .estimatedDelivery(LocalDateTime.now().plusDays(5)) + .createdAt(LocalDateTime.now()) + .build(); + + Shipment savedShipment = shipmentRepository.save(shipment); + + // Update order status to indicate shipment is being processed + orderClient.updateOrderStatus(orderId, "SHIPMENT_CREATED"); + + return savedShipment; + } + + /** + * Updates the status of a shipment. + * + * @param shipmentId The shipment ID + * @param status The new status + * @return The updated shipment, or empty if not found + */ + @Counted(name = "shipmentStatusUpdates", description = "Number of shipment status updates") + public Optional updateShipmentStatus(Long shipmentId, ShipmentStatus status) { + LOGGER.info("Updating shipment " + shipmentId + " status to " + status); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setStatus(status); + shipment.setUpdatedAt(LocalDateTime.now()); + + // If status is SHIPPED, set the shipped date + if (status == ShipmentStatus.SHIPPED) { + shipment.setShippedAt(LocalDateTime.now()); + orderClient.updateOrderStatus(shipment.getOrderId(), "SHIPPED"); + } + // If status is DELIVERED, update order status + else if (status == ShipmentStatus.DELIVERED) { + orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERED"); + } + // If status is FAILED, update order status + else if (status == ShipmentStatus.FAILED) { + orderClient.updateOrderStatus(shipment.getOrderId(), "DELIVERY_FAILED"); + } + + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Gets a shipment by ID. + * + * @param shipmentId The shipment ID + * @return The shipment, or empty if not found + */ + public Optional getShipment(Long shipmentId) { + LOGGER.info("Getting shipment: " + shipmentId); + return shipmentRepository.findById(shipmentId); + } + + /** + * Gets all shipments for an order. + * + * @param orderId The order ID + * @return The list of shipments for the order + */ + public List getShipmentsByOrder(Long orderId) { + LOGGER.info("Getting shipments for order: " + orderId); + return shipmentRepository.findByOrderId(orderId); + } + + /** + * Gets a shipment by tracking number. + * + * @param trackingNumber The tracking number + * @return The shipment, or empty if not found + */ + public Optional getShipmentByTrackingNumber(String trackingNumber) { + LOGGER.info("Getting shipment with tracking number: " + trackingNumber); + List shipments = shipmentRepository.findByTrackingNumber(trackingNumber); + return shipments.isEmpty() ? Optional.empty() : Optional.of(shipments.get(0)); + } + + /** + * Gets all shipments. + * + * @return All shipments + */ + public List getAllShipments() { + LOGGER.info("Getting all shipments"); + return shipmentRepository.findAll(); + } + + /** + * Gets shipments by status. + * + * @param status The status + * @return The list of shipments with the given status + */ + public List getShipmentsByStatus(ShipmentStatus status) { + LOGGER.info("Getting shipments with status: " + status); + return shipmentRepository.findByStatus(status); + } + + /** + * Gets shipments due for delivery by the given date. + * + * @param date The date + * @return The list of shipments due by the given date + */ + public List getShipmentsDueBy(LocalDateTime date) { + LOGGER.info("Getting shipments due by: " + date); + return shipmentRepository.findByEstimatedDeliveryBefore(date); + } + + /** + * Updates the carrier for a shipment. + * + * @param shipmentId The shipment ID + * @param carrier The new carrier + * @return The updated shipment, or empty if not found + */ + public Optional updateCarrier(Long shipmentId, String carrier) { + LOGGER.info("Updating carrier for shipment " + shipmentId + " to " + carrier); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setCarrier(carrier); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the tracking number for a shipment. + * + * @param shipmentId The shipment ID + * @param trackingNumber The new tracking number + * @return The updated shipment, or empty if not found + */ + public Optional updateTrackingNumber(Long shipmentId, String trackingNumber) { + LOGGER.info("Updating tracking number for shipment " + shipmentId + " to " + trackingNumber); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setTrackingNumber(trackingNumber); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the estimated delivery date for a shipment. + * + * @param shipmentId The shipment ID + * @param estimatedDelivery The new estimated delivery date + * @return The updated shipment, or empty if not found + */ + public Optional updateEstimatedDelivery(Long shipmentId, LocalDateTime estimatedDelivery) { + LOGGER.info("Updating estimated delivery for shipment " + shipmentId + " to " + estimatedDelivery); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setEstimatedDelivery(estimatedDelivery); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Updates the notes for a shipment. + * + * @param shipmentId The shipment ID + * @param notes The new notes + * @return The updated shipment, or empty if not found + */ + public Optional updateNotes(Long shipmentId, String notes) { + LOGGER.info("Updating notes for shipment " + shipmentId); + + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + Shipment shipment = shipmentOpt.get(); + shipment.setNotes(notes); + shipment.setUpdatedAt(LocalDateTime.now()); + return Optional.of(shipmentRepository.save(shipment)); + } + + return Optional.empty(); + } + + /** + * Deletes a shipment. + * + * @param shipmentId The shipment ID + * @return true if the shipment was deleted, false if not found + */ + public boolean deleteShipment(Long shipmentId) { + LOGGER.info("Deleting shipment: " + shipmentId); + Optional shipmentOpt = shipmentRepository.findById(shipmentId); + if (shipmentOpt.isPresent()) { + // Only allow deletion if the shipment is in PENDING or PROCESSING status + ShipmentStatus status = shipmentOpt.get().getStatus(); + if (status == ShipmentStatus.PENDING || status == ShipmentStatus.PROCESSING) { + return shipmentRepository.deleteById(shipmentId); + } + LOGGER.warning("Cannot delete shipment with status: " + status); + return false; + } + return false; + } + + /** + * Generates a random tracking number. + * + * @return A random tracking number + */ + private String generateTrackingNumber() { + return String.format("%s-%04d-%04d-%04d", + CARRIERS[RANDOM.nextInt(CARRIERS.length)].substring(0, 2).toUpperCase(), + RANDOM.nextInt(10000), + RANDOM.nextInt(10000), + RANDOM.nextInt(10000)); + } + + /** + * Selects a random carrier. + * + * @return A random carrier + */ + private String selectRandomCarrier() { + return CARRIERS[RANDOM.nextInt(CARRIERS.length)]; + } +} diff --git a/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..5057c129 --- /dev/null +++ b/code/chapter11/shipment/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,32 @@ +# Shipment Service Configuration + +# Order Service URL +order.service.url=http://localhost:8050/order + +# Configure health check properties +mp.health.check.timeout=5s + +# Configure default MP Metrics properties +mp.metrics.tags=app=shipment-service + +# Configure fault tolerance policies +# Retry configuration +mp.fault.tolerance.Retry.delay=1000 +mp.fault.tolerance.Retry.maxRetries=3 +mp.fault.tolerance.Retry.jitter=200 + +# Timeout configuration +mp.fault.tolerance.Timeout.value=5000 + +# Circuit Breaker configuration +mp.fault.tolerance.CircuitBreaker.requestVolumeThreshold=5 +mp.fault.tolerance.CircuitBreaker.failureRatio=0.5 +mp.fault.tolerance.CircuitBreaker.delay=10000 +mp.fault.tolerance.CircuitBreaker.successThreshold=2 + +# Open API configuration +mp.openapi.scan.disable=false +mp.openapi.scan.packages=io.microprofile.tutorial.store.shipment + +# In Docker environment, override the Order service URL +%docker.order.service.url=http://order:8050/order diff --git a/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml b/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ed5b091b --- /dev/null +++ b/code/chapter11/shipment/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + Shipment Service + + + index.html + + + diff --git a/code/chapter11/shipment/src/main/webapp/index.html b/code/chapter11/shipment/src/main/webapp/index.html new file mode 100644 index 00000000..5641acb7 --- /dev/null +++ b/code/chapter11/shipment/src/main/webapp/index.html @@ -0,0 +1,150 @@ + + + + + + Shipment Service - MicroProfile Tutorial + + + +

Shipment Service

+

+ This is the Shipment Service for the MicroProfile Tutorial e-commerce application. + The service manages shipments for orders in the system. +

+ +

REST API

+

+ The service exposes the following endpoints: +

+ +
+

POST /api/shipments/orders/{orderId}

+

Create a new shipment for an order.

+
+ +
+

GET /api/shipments/{shipmentId}

+

Get a shipment by ID.

+
+ +
+

GET /api/shipments

+

Get all shipments.

+
+ +
+

GET /api/shipments/status/{status}

+

Get shipments by status (e.g., PENDING, PROCESSING, SHIPPED, etc.).

+
+ +
+

GET /api/shipments/orders/{orderId}

+

Get all shipments for an order.

+
+ +
+

GET /api/shipments/tracking/{trackingNumber}

+

Get a shipment by tracking number.

+
+ +
+

PUT /api/shipments/{shipmentId}/status/{status}

+

Update the status of a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/carrier

+

Update the carrier for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/tracking

+

Update the tracking number for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/delivery-date

+

Update the estimated delivery date for a shipment.

+
+ +
+

PUT /api/shipments/{shipmentId}/notes

+

Update the notes for a shipment.

+
+ +
+

DELETE /api/shipments/{shipmentId}

+

Delete a shipment (only allowed for shipments in PENDING or PROCESSING status).

+
+ +

OpenAPI Documentation

+

+ The service provides OpenAPI documentation at /shipment/openapi. + You can also access the Swagger UI at /shipment/openapi/ui. +

+ +

Health Checks

+

+ MicroProfile Health endpoints are available at: +

+ + +

Metrics

+

+ MicroProfile Metrics are available at /shipment/metrics. +

+ +
+

Shipment Service - MicroProfile Tutorial E-commerce Application

+
+ + diff --git a/code/chapter11/shoppingcart/Dockerfile b/code/chapter11/shoppingcart/Dockerfile new file mode 100644 index 00000000..c207b409 --- /dev/null +++ b/code/chapter11/shoppingcart/Dockerfile @@ -0,0 +1,20 @@ +FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi + +# Copy configuration files +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Create the apps directory and copy the application +COPY --chown=1001:0 target/shoppingcart.war /config/apps/ + +# Configure the server to run in production mode +RUN configure.sh + +# Expose the default port +EXPOSE 4050 4443 + +# Set the health check +HEALTHCHECK --start-period=60s --interval=10s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:4050/health || exit 1 + +# Run the server +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/shoppingcart/README.md b/code/chapter11/shoppingcart/README.md new file mode 100644 index 00000000..a989bfe8 --- /dev/null +++ b/code/chapter11/shoppingcart/README.md @@ -0,0 +1,87 @@ +# Shopping Cart Service + +This microservice is part of the Jakarta EE and MicroProfile-based e-commerce application. It handles shopping cart management for users. + +## Features + +- Create and manage user shopping carts +- Add products to cart with quantity +- Update and remove cart items +- Check product availability via the Inventory Service +- Fetch product details from the Catalog Service + +## Endpoints + +### GET /shoppingcart/api/carts +- Returns all shopping carts in the system + +### GET /shoppingcart/api/carts/{id} +- Returns a specific shopping cart by ID + +### GET /shoppingcart/api/carts/user/{userId} +- Returns or creates a shopping cart for a specific user + +### POST /shoppingcart/api/carts/user/{userId} +- Creates a new shopping cart for a user + +### POST /shoppingcart/api/carts/{cartId}/items +- Adds an item to a shopping cart +- Request body: CartItem JSON + +### PUT /shoppingcart/api/carts/{cartId}/items/{itemId} +- Updates an item in a shopping cart +- Request body: Updated CartItem JSON + +### DELETE /shoppingcart/api/carts/{cartId}/items/{itemId} +- Removes an item from a shopping cart + +### DELETE /shoppingcart/api/carts/{cartId}/items +- Removes all items from a shopping cart + +### DELETE /shoppingcart/api/carts/{cartId} +- Deletes a shopping cart + +## Cart Item JSON Example + +```json +{ + "productId": 1, + "quantity": 2, + "productName": "Product Name", // Optional, will be fetched from Catalog if not provided + "price": 29.99, // Optional, will be fetched from Catalog if not provided + "imageUrl": "product-image.jpg" // Optional, will be fetched from Catalog if not provided +} +``` + +## Running the Service + +### Local Development + +```bash +./run.sh +``` + +### Docker + +```bash +./run-docker.sh +``` + +## Integration with Other Services + +The Shopping Cart Service integrates with: + +- **Inventory Service**: Checks product availability before adding to cart +- **Catalog Service**: Retrieves product details (name, price, image) +- **Order Service**: Indirectly, when a cart is converted to an order + +## MicroProfile Features Used + +- **Config**: For service URL configuration +- **Fault Tolerance**: Circuit breakers, timeouts, retries, and fallbacks for resilient communication +- **Health**: Liveness and readiness checks +- **OpenAPI**: API documentation + +## Swagger UI + +OpenAPI documentation is available at: `http://localhost:4050/shoppingcart/api/openapi-ui/` diff --git a/code/chapter11/shoppingcart/pom.xml b/code/chapter11/shoppingcart/pom.xml new file mode 100644 index 00000000..df9eae09 --- /dev/null +++ b/code/chapter11/shoppingcart/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.microprofile + shoppingcart + 1.0-SNAPSHOT + war + + shopping-cart-service + https://microprofile.io + + + UTF-8 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + shoppingcart + + + + io.openliberty.tools + liberty-maven-plugin + 3.8.2 + + shoppingcartServer + runnable + 120 + + /shoppingcart + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/shoppingcart/run-docker.sh b/code/chapter11/shoppingcart/run-docker.sh new file mode 100644 index 00000000..6b32df82 --- /dev/null +++ b/code/chapter11/shoppingcart/run-docker.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script to build and run the Shopping Cart service in Docker + +# Stop execution on any error +set -e + +# Navigate to the shopping cart service directory +cd "$(dirname "$0")" + +# Build the project with Maven +echo "Building with Maven..." +mvn clean package + +# Build the Docker image +echo "Building Docker image..." +docker build -t shoppingcart-service . + +# Run the Docker container +echo "Starting Docker container..." +docker run -d --name shoppingcart-service -p 4050:4050 shoppingcart-service + +echo "Shopping Cart service is running on http://localhost:4050/shoppingcart" diff --git a/code/chapter11/shoppingcart/run.sh b/code/chapter11/shoppingcart/run.sh new file mode 100644 index 00000000..02b3ee65 --- /dev/null +++ b/code/chapter11/shoppingcart/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Script to build and run the Shopping Cart service + +# Stop execution on any error +set -e + +echo "Building and running Shopping Cart service..." + +# Navigate to the shopping cart service directory +cd "$(dirname "$0")" + +# Build the project with Maven +echo "Building with Maven..." +mvn clean package + +# Run the application using Liberty Maven plugin +echo "Starting Liberty server..." +mvn liberty:run diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java new file mode 100644 index 00000000..84cfe0d6 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/ShoppingCartApplication.java @@ -0,0 +1,12 @@ +package io.microprofile.tutorial.store.shoppingcart; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * REST application for shopping cart management. + */ +@ApplicationPath("/api") +public class ShoppingCartApplication extends Application { + // The resources will be discovered automatically +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java new file mode 100644 index 00000000..e13684cc --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java @@ -0,0 +1,184 @@ +package io.microprofile.tutorial.store.shoppingcart.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Catalog Service. + */ +@ApplicationScoped +public class CatalogClient { + + private static final Logger LOGGER = Logger.getLogger(CatalogClient.class.getName()); + + @ConfigProperty(name = "catalog.service.url", defaultValue = "http://localhost:5050/catalog") + private String catalogServiceUrl; + + // Cache for product details to reduce service calls + private final Map productCache = new HashMap<>(); + + /** + * Gets product information from the catalog service. + * + * @param productId The product ID + * @return ProductInfo containing product details + */ + // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) + // @Timeout(value = 5, unit = ChronoUnit.SECONDS) + // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + // @Fallback(fallbackMethod = "getProductInfoFallback") + public ProductInfo getProductInfo(Long productId) { + // Check cache first + if (productCache.containsKey(productId)) { + return productCache.get(productId); + } + + LOGGER.info(String.format("Fetching product info for product %d", productId)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/products/%d", catalogServiceUrl, productId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple parsing - in a real app, use proper JSON parsing + String name = extractField(jsonResponse, "name"); + String priceStr = extractField(jsonResponse, "price"); + + double price = 0.0; + try { + price = Double.parseDouble(priceStr); + } catch (NumberFormatException e) { + LOGGER.warning("Failed to parse product price: " + priceStr); + } + + ProductInfo productInfo = new ProductInfo(productId, name, price); + + // Cache the result + productCache.put(productId, productInfo); + + return productInfo; + } + + LOGGER.warning(String.format("Failed to get product info. Status code: %d", response.getStatus())); + return new ProductInfo(productId, "Unknown Product", 0.0); + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Catalog Service", e); + throw e; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for getProductInfo. + * Returns a placeholder product info when the catalog service is unavailable. + * + * @param productId The product ID + * @return A placeholder ProductInfo object + */ + public ProductInfo getProductInfoFallback(Long productId) { + LOGGER.warning(String.format("Using fallback for product info. Product ID: %d", productId)); + + // Check if we have a cached version + if (productCache.containsKey(productId)) { + return productCache.get(productId); + } + + // Return a placeholder + return new ProductInfo( + productId, + "Product " + productId + " (Service Unavailable)", + 0.0 + ); + } + + /** + * Helper method to extract field values from JSON string. + * This is a simplified approach - in a real app, use a proper JSON parser. + * + * @param jsonString The JSON string + * @param fieldName The name of the field to extract + * @return The extracted field value + */ + private String extractField(String jsonString, String fieldName) { + String searchPattern = "\"" + fieldName + "\":"; + if (jsonString.contains(searchPattern)) { + int startIndex = jsonString.indexOf(searchPattern) + searchPattern.length(); + int endIndex; + + // Skip whitespace + while (startIndex < jsonString.length() && + (jsonString.charAt(startIndex) == ' ' || jsonString.charAt(startIndex) == '\t')) { + startIndex++; + } + + if (startIndex < jsonString.length() && jsonString.charAt(startIndex) == '"') { + // String value + startIndex++; // Skip opening quote + endIndex = jsonString.indexOf("\"", startIndex); + } else { + // Number or boolean value + endIndex = jsonString.indexOf(",", startIndex); + if (endIndex == -1) { + endIndex = jsonString.indexOf("}", startIndex); + } + } + + if (endIndex > startIndex) { + return jsonString.substring(startIndex, endIndex); + } + } + return ""; + } + + /** + * Inner class to hold product information. + */ + public static class ProductInfo { + private final Long productId; + private final String name; + private final double price; + + public ProductInfo(Long productId, String name, double price) { + this.productId = productId; + this.name = name; + this.price = price; + } + + public Long getProductId() { + return productId; + } + + public String getName() { + return name; + } + + public double getPrice() { + return price; + } + } +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java new file mode 100644 index 00000000..b9ac4c04 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java @@ -0,0 +1,96 @@ +package io.microprofile.tutorial.store.shoppingcart.client; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client for communicating with the Inventory Service. + */ +@ApplicationScoped +public class InventoryClient { + + private static final Logger LOGGER = Logger.getLogger(InventoryClient.class.getName()); + + @ConfigProperty(name = "inventory.service.url", defaultValue = "http://localhost:7050/inventory") + private String inventoryServiceUrl; + + /** + * Checks if a product is available in sufficient quantity. + * + * @param productId The product ID + * @param quantity The requested quantity + * @return true if the product is available in the requested quantity, false otherwise + */ + // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) + // @Timeout(value = 5, unit = ChronoUnit.SECONDS) + // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + // @Fallback(fallbackMethod = "checkProductAvailabilityFallback") + public boolean checkProductAvailability(Long productId, int quantity) { + LOGGER.info(String.format("Checking availability for product %d, quantity %d", productId, quantity)); + + Client client = null; + try { + client = ClientBuilder.newClient(); + String url = String.format("%s/api/inventories/product/%d", inventoryServiceUrl, productId); + + Response response = client.target(url) + .request(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + String jsonResponse = response.readEntity(String.class); + // Simple parsing - in a real app, use proper JSON parsing + if (jsonResponse.contains("\"quantity\":")) { + String quantityStr = jsonResponse.split("\"quantity\":")[1].split(",")[0].trim(); + int availableQuantity = Integer.parseInt(quantityStr); + return availableQuantity >= quantity; + } + } + + LOGGER.warning(String.format("Failed to check product availability. Status code: %d", response.getStatus())); + return false; + } catch (ProcessingException e) { + LOGGER.log(Level.SEVERE, "Error connecting to Inventory Service", e); + throw e; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error parsing inventory response", e); + return false; + } finally { + if (client != null) { + client.close(); + } + } + } + + /** + * Fallback method for checkProductAvailability. + * Always returns true to allow the cart operation to continue, + * but logs a warning. + * + * @param productId The product ID + * @param quantity The requested quantity + * @return true, allowing the operation to proceed + */ + public boolean checkProductAvailabilityFallback(Long productId, int quantity) { + LOGGER.warning(String.format( + "Using fallback for product availability check. Product ID: %d, Quantity: %d", + productId, quantity)); + // In a production system, you might want to cache product availability + // or implement a more sophisticated fallback mechanism + return true; // Allow the operation to proceed + } +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java new file mode 100644 index 00000000..dc4537ed --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/CartItem.java @@ -0,0 +1,32 @@ +package io.microprofile.tutorial.store.shoppingcart.entity; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * CartItem class for the microprofile tutorial store application. + * This class represents an item in a shopping cart. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CartItem { + + private Long itemId; + + @NotNull(message = "Product ID cannot be null") + private Long productId; + + private String productName; + + @Min(value = 0, message = "Price must be greater than or equal to 0") + private double price; + + @Min(value = 1, message = "Quantity must be at least 1") + private int quantity; +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java new file mode 100644 index 00000000..08f1c0a8 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/entity/ShoppingCart.java @@ -0,0 +1,57 @@ +package io.microprofile.tutorial.store.shoppingcart.entity; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * ShoppingCart class for the microprofile tutorial store application. + * This class represents a user's shopping cart. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ShoppingCart { + + private Long cartId; + + @NotNull(message = "User ID cannot be null") + private Long userId; + + @Builder.Default + private List items = new ArrayList<>(); + + @Builder.Default + private LocalDateTime createdAt = LocalDateTime.now(); + + private LocalDateTime updatedAt; + + /** + * Calculate the total number of items in the cart. + * + * @return The total number of items + */ + public int getTotalItems() { + return items.stream() + .mapToInt(CartItem::getQuantity) + .sum(); + } + + /** + * Calculate the total price of all items in the cart. + * + * @return The total price + */ + public double getTotalPrice() { + return items.stream() + .mapToDouble(item -> item.getPrice() * item.getQuantity()) + .sum(); + } +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java new file mode 100644 index 00000000..91dc8339 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java @@ -0,0 +1,68 @@ +// package io.microprofile.tutorial.store.shoppingcart.health; + +// import jakarta.enterprise.context.ApplicationScoped; +// import jakarta.inject.Inject; + +// import org.eclipse.microprofile.health.HealthCheck; +// import org.eclipse.microprofile.health.HealthCheckResponse; +// import org.eclipse.microprofile.health.Liveness; +// import org.eclipse.microprofile.health.Readiness; + +// import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; + +// /** +// * Health checks for the Shopping Cart service. +// */ +// @ApplicationScoped +// public class ShoppingCartHealthCheck { + +// @Inject +// private ShoppingCartRepository cartRepository; + +// /** +// * Liveness check for the Shopping Cart service. +// * This check ensures that the application is running. +// * +// * @return A HealthCheckResponse indicating whether the service is alive +// */ +// @Liveness +// public HealthCheck shoppingCartLivenessCheck() { +// return () -> HealthCheckResponse.named("shopping-cart-service-liveness") +// .up() +// .withData("message", "Shopping Cart Service is alive") +// .build(); +// } + +// /** +// * Readiness check for the Shopping Cart service. +// * This check ensures that the application is ready to serve requests. +// * In a real application, this would check dependencies like databases. +// * +// * @return A HealthCheckResponse indicating whether the service is ready +// */ +// @Readiness +// public HealthCheck shoppingCartReadinessCheck() { +// boolean isReady = true; + +// try { +// // Simple check to ensure repository is functioning +// cartRepository.findAll(); +// } catch (Exception e) { +// isReady = false; +// } + +// return () -> { +// if (isReady) { +// return HealthCheckResponse.named("shopping-cart-service-readiness") +// .up() +// .withData("message", "Shopping Cart Service is ready") +// .build(); +// } else { +// return HealthCheckResponse.named("shopping-cart-service-readiness") +// .down() +// .withData("message", "Shopping Cart Service is not ready") +// .build(); +// } +// }; +// } +// } diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java new file mode 100644 index 00000000..90b3c65a --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/repository/ShoppingCartRepository.java @@ -0,0 +1,199 @@ +package io.microprofile.tutorial.store.shoppingcart.repository; + +import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; +import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Simple in-memory repository for ShoppingCart objects. + * This class provides operations for shopping cart management. + */ +@ApplicationScoped +public class ShoppingCartRepository { + + private final Map carts = new ConcurrentHashMap<>(); + private final Map> cartItems = new ConcurrentHashMap<>(); + private long nextCartId = 1; + private long nextItemId = 1; + + /** + * Finds a shopping cart by user ID. + * + * @param userId The user ID + * @return An Optional containing the shopping cart if found, or empty if not found + */ + public Optional findByUserId(Long userId) { + return carts.values().stream() + .filter(cart -> cart.getUserId().equals(userId)) + .findFirst(); + } + + /** + * Finds a shopping cart by cart ID. + * + * @param cartId The cart ID + * @return An Optional containing the shopping cart if found, or empty if not found + */ + public Optional findById(Long cartId) { + return Optional.ofNullable(carts.get(cartId)); + } + + /** + * Creates a new shopping cart for a user. + * + * @param userId The user ID + * @return The created shopping cart + */ + public ShoppingCart createCart(Long userId) { + ShoppingCart cart = ShoppingCart.builder() + .cartId(nextCartId++) + .userId(userId) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + carts.put(cart.getCartId(), cart); + cartItems.put(cart.getCartId(), new HashMap<>()); + + return cart; + } + + /** + * Adds an item to a shopping cart. + * If the product already exists in the cart, the quantity is increased. + * + * @param cartId The cart ID + * @param item The item to add + * @return The updated cart item + */ + public CartItem addItem(Long cartId, CartItem item) { + Map items = cartItems.get(cartId); + if (items == null) { + throw new IllegalArgumentException("Cart not found: " + cartId); + } + + // Check if the product already exists in the cart + Optional existingItem = items.values().stream() + .filter(i -> i.getProductId().equals(item.getProductId())) + .findFirst(); + + if (existingItem.isPresent()) { + // Update existing item quantity + CartItem updatedItem = existingItem.get(); + updatedItem.setQuantity(updatedItem.getQuantity() + item.getQuantity()); + items.put(updatedItem.getItemId(), updatedItem); + updateCartItems(cartId); + return updatedItem; + } else { + // Add new item + if (item.getItemId() == null) { + item.setItemId(nextItemId++); + } + items.put(item.getItemId(), item); + updateCartItems(cartId); + return item; + } + } + + /** + * Updates an item in a shopping cart. + * + * @param cartId The cart ID + * @param itemId The item ID + * @param item The updated item + * @return The updated cart item + */ + public CartItem updateItem(Long cartId, Long itemId, CartItem item) { + Map items = cartItems.get(cartId); + if (items == null || !items.containsKey(itemId)) { + throw new IllegalArgumentException("Item not found in cart"); + } + + item.setItemId(itemId); + items.put(itemId, item); + updateCartItems(cartId); + + return item; + } + + /** + * Removes an item from a shopping cart. + * + * @param cartId The cart ID + * @param itemId The item ID + * @return true if the item was removed, false otherwise + */ + public boolean removeItem(Long cartId, Long itemId) { + Map items = cartItems.get(cartId); + if (items == null) { + return false; + } + + boolean removed = items.remove(itemId) != null; + if (removed) { + updateCartItems(cartId); + } + + return removed; + } + + /** + * Clears all items from a shopping cart. + * + * @param cartId The cart ID + * @return true if the cart was cleared, false if the cart wasn't found + */ + public boolean clearCart(Long cartId) { + Map items = cartItems.get(cartId); + if (items == null) { + return false; + } + + items.clear(); + updateCartItems(cartId); + + return true; + } + + /** + * Deletes a shopping cart. + * + * @param cartId The cart ID + * @return true if the cart was deleted, false if not found + */ + public boolean deleteCart(Long cartId) { + cartItems.remove(cartId); + return carts.remove(cartId) != null; + } + + /** + * Gets all shopping carts. + * + * @return A list of all shopping carts + */ + public List findAll() { + return new ArrayList<>(carts.values()); + } + + /** + * Updates the items list in a shopping cart and updates the timestamp. + * + * @param cartId The cart ID + */ + private void updateCartItems(Long cartId) { + ShoppingCart cart = carts.get(cartId); + if (cart != null) { + cart.setItems(new ArrayList<>(cartItems.get(cartId).values())); + cart.setUpdatedAt(LocalDateTime.now()); + } + } +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java new file mode 100644 index 00000000..ec40e55b --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/resource/ShoppingCartResource.java @@ -0,0 +1,240 @@ +package io.microprofile.tutorial.store.shoppingcart.resource; + +import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; +import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; +import io.microprofile.tutorial.store.shoppingcart.service.ShoppingCartService; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.net.URI; +import java.util.List; + +/** + * REST resource for shopping cart operations. + */ +@Path("/carts") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Shopping Cart Resource", description = "Shopping cart management operations") +public class ShoppingCartResource { + + @Inject + private ShoppingCartService cartService; + + @Context + private UriInfo uriInfo; + + @GET + @Operation(summary = "Get all shopping carts", description = "Returns a list of all shopping carts") + @APIResponse( + responseCode = "200", + description = "List of shopping carts", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = ShoppingCart.class) + ) + ) + public List getAllCarts() { + return cartService.getAllCarts(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Get cart by ID", description = "Returns a specific shopping cart by ID") + @APIResponse( + responseCode = "200", + description = "Shopping cart", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ShoppingCart.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Cart not found" + ) + public ShoppingCart getCartById( + @Parameter(description = "ID of the cart", required = true) + @PathParam("id") Long cartId) { + return cartService.getCartById(cartId); + } + + @GET + @Path("/user/{userId}") + @Operation(summary = "Get cart by user ID", description = "Returns a user's shopping cart") + @APIResponse( + responseCode = "200", + description = "Shopping cart", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ShoppingCart.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Cart not found for user" + ) + public Response getCartByUserId( + @Parameter(description = "ID of the user", required = true) + @PathParam("userId") Long userId) { + try { + ShoppingCart cart = cartService.getCartByUserId(userId); + return Response.ok(cart).build(); + } catch (WebApplicationException e) { + if (e.getResponse().getStatus() == Response.Status.NOT_FOUND.getStatusCode()) { + // Create a new cart for the user + ShoppingCart newCart = cartService.getOrCreateCart(userId); + return Response.ok(newCart).build(); + } + throw e; + } + } + + @POST + @Path("/user/{userId}") + @Operation(summary = "Create cart for user", description = "Creates a new shopping cart for a user") + @APIResponse( + responseCode = "201", + description = "Cart created", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = ShoppingCart.class) + ) + ) + public Response createCartForUser( + @Parameter(description = "ID of the user", required = true) + @PathParam("userId") Long userId) { + ShoppingCart cart = cartService.getOrCreateCart(userId); + URI location = uriInfo.getAbsolutePathBuilder().path(cart.getCartId().toString()).build(); + return Response.created(location).entity(cart).build(); + } + + @POST + @Path("/{cartId}/items") + @Operation(summary = "Add item to cart", description = "Adds an item to a shopping cart") + @APIResponse( + responseCode = "200", + description = "Item added", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CartItem.class) + ) + ) + @APIResponse( + responseCode = "400", + description = "Invalid input or insufficient inventory" + ) + @APIResponse( + responseCode = "404", + description = "Cart not found" + ) + public CartItem addItemToCart( + @Parameter(description = "ID of the cart", required = true) + @PathParam("cartId") Long cartId, + @Parameter(description = "Item to add", required = true) + @NotNull @Valid CartItem item) { + return cartService.addItemToCart(cartId, item); + } + + @PUT + @Path("/{cartId}/items/{itemId}") + @Operation(summary = "Update cart item", description = "Updates an item in a shopping cart") + @APIResponse( + responseCode = "200", + description = "Item updated", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CartItem.class) + ) + ) + @APIResponse( + responseCode = "400", + description = "Invalid input or insufficient inventory" + ) + @APIResponse( + responseCode = "404", + description = "Cart or item not found" + ) + public CartItem updateCartItem( + @Parameter(description = "ID of the cart", required = true) + @PathParam("cartId") Long cartId, + @Parameter(description = "ID of the item", required = true) + @PathParam("itemId") Long itemId, + @Parameter(description = "Updated item", required = true) + @NotNull @Valid CartItem item) { + return cartService.updateCartItem(cartId, itemId, item); + } + + @DELETE + @Path("/{cartId}/items/{itemId}") + @Operation(summary = "Remove item from cart", description = "Removes an item from a shopping cart") + @APIResponse( + responseCode = "204", + description = "Item removed" + ) + @APIResponse( + responseCode = "404", + description = "Cart or item not found" + ) + public Response removeItemFromCart( + @Parameter(description = "ID of the cart", required = true) + @PathParam("cartId") Long cartId, + @Parameter(description = "ID of the item", required = true) + @PathParam("itemId") Long itemId) { + cartService.removeItemFromCart(cartId, itemId); + return Response.noContent().build(); + } + + @DELETE + @Path("/{cartId}/items") + @Operation(summary = "Clear cart", description = "Removes all items from a shopping cart") + @APIResponse( + responseCode = "204", + description = "Cart cleared" + ) + @APIResponse( + responseCode = "404", + description = "Cart not found" + ) + public Response clearCart( + @Parameter(description = "ID of the cart", required = true) + @PathParam("cartId") Long cartId) { + cartService.clearCart(cartId); + return Response.noContent().build(); + } + + @DELETE + @Path("/{cartId}") + @Operation(summary = "Delete cart", description = "Deletes a shopping cart") + @APIResponse( + responseCode = "204", + description = "Cart deleted" + ) + @APIResponse( + responseCode = "404", + description = "Cart not found" + ) + public Response deleteCart( + @Parameter(description = "ID of the cart", required = true) + @PathParam("cartId") Long cartId) { + cartService.deleteCart(cartId); + return Response.noContent().build(); + } +} diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java new file mode 100644 index 00000000..bc39375f --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/service/ShoppingCartService.java @@ -0,0 +1,223 @@ +package io.microprofile.tutorial.store.shoppingcart.service; + +import io.microprofile.tutorial.store.shoppingcart.client.CatalogClient; +import io.microprofile.tutorial.store.shoppingcart.client.InventoryClient; +import io.microprofile.tutorial.store.shoppingcart.entity.CartItem; +import io.microprofile.tutorial.store.shoppingcart.entity.ShoppingCart; +import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Service class for Shopping Cart management operations. + */ +@ApplicationScoped +public class ShoppingCartService { + + private static final Logger LOGGER = Logger.getLogger(ShoppingCartService.class.getName()); + + @Inject + private ShoppingCartRepository cartRepository; + + @Inject + private InventoryClient inventoryClient; + + @Inject + private CatalogClient catalogClient; + + /** + * Gets a shopping cart for a user, creating one if it doesn't exist. + * + * @param userId The user ID + * @return The user's shopping cart + */ + public ShoppingCart getOrCreateCart(Long userId) { + Optional existingCart = cartRepository.findByUserId(userId); + + return existingCart.orElseGet(() -> { + LOGGER.info("Creating new cart for user: " + userId); + return cartRepository.createCart(userId); + }); + } + + /** + * Gets a shopping cart by ID. + * + * @param cartId The cart ID + * @return The shopping cart + * @throws WebApplicationException if the cart is not found + */ + public ShoppingCart getCartById(Long cartId) { + return cartRepository.findById(cartId) + .orElseThrow(() -> new WebApplicationException("Cart not found", Response.Status.NOT_FOUND)); + } + + /** + * Gets a user's shopping cart. + * + * @param userId The user ID + * @return The user's shopping cart + * @throws WebApplicationException if the cart is not found + */ + public ShoppingCart getCartByUserId(Long userId) { + return cartRepository.findByUserId(userId) + .orElseThrow(() -> new WebApplicationException("Cart not found for user", Response.Status.NOT_FOUND)); + } + + /** + * Gets all shopping carts. + * + * @return A list of all shopping carts + */ + public List getAllCarts() { + return cartRepository.findAll(); + } + + /** + * Adds an item to a shopping cart. + * + * @param cartId The cart ID + * @param item The item to add + * @return The updated cart item + * @throws WebApplicationException if the cart is not found or inventory is insufficient + */ + public CartItem addItemToCart(Long cartId, CartItem item) { + // Verify the cart exists + getCartById(cartId); + + // Check inventory availability + boolean isAvailable = inventoryClient.checkProductAvailability(item.getProductId(), item.getQuantity()); + if (!isAvailable) { + throw new WebApplicationException("Insufficient inventory for product: " + item.getProductId(), + Response.Status.BAD_REQUEST); + } + + // Enrich item with product details if needed + if (item.getProductName() == null || item.getPrice() == 0) { + CatalogClient.ProductInfo productInfo = catalogClient.getProductInfo(item.getProductId()); + item.setProductName(productInfo.getName()); + item.setPrice(productInfo.getPrice()); + } + + LOGGER.info(String.format("Adding item to cart %d: %s, quantity %d", + cartId, item.getProductName(), item.getQuantity())); + + return cartRepository.addItem(cartId, item); + } + + /** + * Updates an item in a shopping cart. + * + * @param cartId The cart ID + * @param itemId The item ID + * @param item The updated item + * @return The updated cart item + * @throws WebApplicationException if the cart or item is not found or inventory is insufficient + */ + public CartItem updateCartItem(Long cartId, Long itemId, CartItem item) { + // Verify the cart exists + ShoppingCart cart = getCartById(cartId); + + // Verify the item exists + Optional existingItem = cart.getItems().stream() + .filter(i -> i.getItemId().equals(itemId)) + .findFirst(); + + if (!existingItem.isPresent()) { + throw new WebApplicationException("Item not found in cart", Response.Status.NOT_FOUND); + } + + // Check inventory availability if quantity is increasing + CartItem currentItem = existingItem.get(); + if (item.getQuantity() > currentItem.getQuantity()) { + int additionalQuantity = item.getQuantity() - currentItem.getQuantity(); + boolean isAvailable = inventoryClient.checkProductAvailability( + currentItem.getProductId(), additionalQuantity); + + if (!isAvailable) { + throw new WebApplicationException("Insufficient inventory for product: " + currentItem.getProductId(), + Response.Status.BAD_REQUEST); + } + } + + // Preserve product information + item.setProductId(currentItem.getProductId()); + + // If no product name is provided, use the existing one + if (item.getProductName() == null) { + item.setProductName(currentItem.getProductName()); + } + + // If no price is provided, use the existing one + if (item.getPrice() == 0) { + item.setPrice(currentItem.getPrice()); + } + + LOGGER.info(String.format("Updating item %d in cart %d: new quantity %d", + itemId, cartId, item.getQuantity())); + + return cartRepository.updateItem(cartId, itemId, item); + } + + /** + * Removes an item from a shopping cart. + * + * @param cartId The cart ID + * @param itemId The item ID + * @throws WebApplicationException if the cart or item is not found + */ + public void removeItemFromCart(Long cartId, Long itemId) { + // Verify the cart exists + getCartById(cartId); + + boolean removed = cartRepository.removeItem(cartId, itemId); + if (!removed) { + throw new WebApplicationException("Item not found in cart", Response.Status.NOT_FOUND); + } + + LOGGER.info(String.format("Removed item %d from cart %d", itemId, cartId)); + } + + /** + * Clears all items from a shopping cart. + * + * @param cartId The cart ID + * @throws WebApplicationException if the cart is not found + */ + public void clearCart(Long cartId) { + // Verify the cart exists + getCartById(cartId); + + boolean cleared = cartRepository.clearCart(cartId); + if (!cleared) { + throw new WebApplicationException("Failed to clear cart", Response.Status.INTERNAL_SERVER_ERROR); + } + + LOGGER.info(String.format("Cleared cart %d", cartId)); + } + + /** + * Deletes a shopping cart. + * + * @param cartId The cart ID + * @throws WebApplicationException if the cart is not found + */ + public void deleteCart(Long cartId) { + // Verify the cart exists + getCartById(cartId); + + boolean deleted = cartRepository.deleteCart(cartId); + if (!deleted) { + throw new WebApplicationException("Failed to delete cart", Response.Status.INTERNAL_SERVER_ERROR); + } + + LOGGER.info(String.format("Deleted cart %d", cartId)); + } +} diff --git a/code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..9990f3d1 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,16 @@ +# Shopping Cart Service Configuration +mp.openapi.scan=true + +# Service URLs +inventory.service.url=https://scaling-pancake-77vj4pwq7fpjqx-7050.app.github.dev/ +catalog.service.url=https://scaling-pancake-77vj4pwq7fpjqx-5050.app.github.dev/ +user.service.url=https://scaling-pancake-77vj4pwq7fpjqx-6050.app.github.dev/ + +# Fault Tolerance Configuration +# circuitBreaker.delay=10000 +# circuitBreaker.requestVolumeThreshold=4 +# circuitBreaker.failureRatio=0.5 +# retry.maxRetries=3 +# retry.delay=1000 +# retry.jitter=200 +# timeout.value=5000 diff --git a/code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml b/code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..383982d7 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + Shopping Cart Service + + + index.html + index.jsp + + diff --git a/code/chapter11/shoppingcart/src/main/webapp/index.html b/code/chapter11/shoppingcart/src/main/webapp/index.html new file mode 100644 index 00000000..d2d25193 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/webapp/index.html @@ -0,0 +1,128 @@ + + + + + + Shopping Cart Service - MicroProfile E-Commerce + + + +
+

Shopping Cart Service

+

Part of the MicroProfile E-Commerce Application

+
+ +
+
+

About this Service

+

The Shopping Cart Service manages user shopping carts in the e-commerce system.

+

It provides endpoints for creating carts, adding/removing items, and managing cart contents.

+

This service integrates with the Inventory service to check product availability and the Catalog service to get product details.

+
+ +
+

API Endpoints

+
    +
  • GET /api/carts - Get all shopping carts
  • +
  • GET /api/carts/{id} - Get cart by ID
  • +
  • GET /api/carts/user/{userId} - Get cart by user ID
  • +
  • POST /api/carts/user/{userId} - Create cart for user
  • +
  • POST /api/carts/{cartId}/items - Add item to cart
  • +
  • PUT /api/carts/{cartId}/items/{itemId} - Update cart item
  • +
  • DELETE /api/carts/{cartId}/items/{itemId} - Remove item from cart
  • +
  • DELETE /api/carts/{cartId}/items - Clear cart
  • +
  • DELETE /api/carts/{cartId} - Delete cart
  • +
+
+ + +
+ +
+

MicroProfile E-Commerce Demo Application | Shopping Cart Service

+

Powered by Open Liberty & MicroProfile

+
+ + diff --git a/code/chapter11/shoppingcart/src/main/webapp/index.jsp b/code/chapter11/shoppingcart/src/main/webapp/index.jsp new file mode 100644 index 00000000..1fcd4190 --- /dev/null +++ b/code/chapter11/shoppingcart/src/main/webapp/index.jsp @@ -0,0 +1,12 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> + + + + + + Redirecting... + + +

Redirecting to the Shopping Cart Service homepage...

+ + diff --git a/code/chapter11/user/Dockerfile b/code/chapter11/user/Dockerfile new file mode 100644 index 00000000..3faf2311 --- /dev/null +++ b/code/chapter11/user/Dockerfile @@ -0,0 +1,19 @@ +FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi + +# Copy Liberty configuration +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +# Copy application WAR file +COPY --chown=1001:0 target/user.war /config/apps/ + +# Set environment variables +ENV PORT=9080 + +# Configure the server to run +RUN configure.sh + +# Expose ports +EXPOSE 6050 6051 + +# Start the server +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/user/README.adoc b/code/chapter11/user/README.adoc new file mode 100644 index 00000000..fdcc577f --- /dev/null +++ b/code/chapter11/user/README.adoc @@ -0,0 +1,280 @@ += User Management Service +:toc: left +:icons: font +:source-highlighter: highlightjs +:sectnums: +:imagesdir: images + +This document provides information about the User Management Service, part of the MicroProfile tutorial store application. + +== Overview + +The User Management Service is responsible for user operations including: + +* User registration and management +* User profile information +* Basic authentication + +This service demonstrates MicroProfile and Jakarta EE technologies in a microservice architecture. + +== Technology Stack + +The User Management Service uses the following technologies: + +* Jakarta EE 10 +** RESTful Web Services (JAX-RS 3.1) +** Context and Dependency Injection (CDI 4.0) +** Bean Validation 3.0 +** JSON-B 3.0 +* MicroProfile 6.1 +** OpenAPI 3.1 +* Open Liberty +* Maven + +== Project Structure + +[source] +---- +user/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── io/microprofile/tutorial/store/user/ +│ │ │ ├── entity/ # Domain objects +│ │ │ ├── exception/ # Custom exceptions +│ │ │ ├── repository/ # Data access layer +│ │ │ ├── resource/ # REST endpoints +│ │ │ ├── service/ # Business logic +│ │ │ └── UserApplication.java +│ │ ├── liberty/ +│ │ │ └── config/ +│ │ │ └── server.xml # Liberty server configuration +│ │ ├── resources/ +│ │ │ └── META-INF/ +│ │ │ └── microprofile-config.properties +│ │ └── webapp/ +│ │ └── index.html # Welcome page +│ └── test/ # Unit and integration tests +└── pom.xml # Maven configuration +---- + +== API Endpoints + +The service exposes the following RESTful endpoints: + +[cols="2,1,4", options="header"] +|=== +| Endpoint | Method | Description + +| `/api/users` | GET | Retrieve all users +| `/api/users/{id}` | GET | Retrieve a specific user by ID +| `/api/users` | POST | Create a new user +| `/api/users/{id}` | PUT | Update an existing user +| `/api/users/{id}` | DELETE | Delete a user +|=== + +== Running the Service + +=== Prerequisites + +* JDK 17 or later +* Maven 3.8+ +* Docker (optional, for containerized deployment) + +=== Local Development + +1. Clone the repository: ++ +[source,bash] +---- +git clone https://github.com/your-org/liberty-rest-app.git +cd liberty-rest-app/user +---- + +2. Build the project: ++ +[source,bash] +---- +mvn clean package +---- + +3. Run the service: ++ +[source,bash] +---- +mvn liberty:run +---- + +4. The service will be available at: ++ +[source] +---- +http://localhost:6050/user/api/users +---- + +=== Docker Deployment + +To build and run using Docker: + +[source,bash] +---- +# Build the Docker image +docker build -t microprofile-tutorial/user-service . + +# Run the container +docker run -p 6050:6050 microprofile-tutorial/user-service +---- + +== Configuration + +The service can be configured using Liberty server.xml and MicroProfile Config: + +=== server.xml + +The main configuration file at `src/main/liberty/config/server.xml` includes: + +* HTTP endpoint configuration (port 6050) +* Feature enablement +* Application context configuration + +=== MicroProfile Config + +Environment-specific configuration can be modified in: +`src/main/resources/META-INF/microprofile-config.properties` + +== OpenAPI Documentation + +The service provides OpenAPI documentation of all endpoints. + +Access the OpenAPI UI at: +[source] +---- +http://localhost:6050/openapi/ui +---- + +Raw OpenAPI specification: +[source] +---- +http://localhost:6050/openapi +---- + +== Exception Handling + +The service includes a comprehensive exception handling strategy: + +* Custom exceptions for domain-specific errors +* Global exception mapping to appropriate HTTP status codes +* Consistent error response format + +Error responses follow this structure: + +[source,json] +---- +{ + "errorCode": "user_not_found", + "message": "User with ID 123 not found", + "timestamp": "2023-04-15T14:30:45Z" +} +---- + +Common error scenarios: + +* 400 Bad Request - Invalid input data +* 404 Not Found - Requested user doesn't exist +* 409 Conflict - Email address already in use + +== Testing + +=== Running Tests + +Execute unit and integration tests with: + +[source,bash] +---- +mvn test +---- + +=== Testing with cURL + +*Get all users:* +[source,bash] +---- +curl -X GET http://localhost:6050/user/api/users +---- + +*Get user by ID:* +[source,bash] +---- +curl -X GET http://localhost:6050/user/api/users/1 +---- + +*Create new user:* +[source,bash] +---- +curl -X POST http://localhost:6050/user/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "passwordHash": "hashed_password", + "address": "123 Main St", + "phone": "+1234567890" + }' +---- + +*Update user:* +[source,bash] +---- +curl -X PUT http://localhost:6050/user/api/users/1 \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Updated", + "email": "john@example.com", + "passwordHash": "hashed_password", + "address": "456 New Address", + "phone": "+1234567890" + }' +---- + +*Delete user:* +[source,bash] +---- +curl -X DELETE http://localhost:6050/user/api/users/1 +---- + +== Implementation Notes + +=== In-Memory Storage + +The service currently uses thread-safe in-memory storage: + +* `ConcurrentHashMap` for storing user data +* `AtomicLong` for generating sequence IDs +* No persistence to external databases + +For production use, consider implementing a proper database persistence layer. + +=== Security Considerations + +* Passwords are stored as hashes (not encrypted or in plain text) +* Input validation helps prevent injection attacks +* No authentication mechanism is implemented (for demo purposes only) + +== Troubleshooting + +=== Common Issues + +* *Port conflicts:* Check if port 6050 is already in use +* *CORS issues:* For browser access, check CORS configuration in server.xml +* *404 errors:* Verify the application context root and API path + +=== Logs + +* Liberty server logs are in `target/liberty/wlp/usr/servers/defaultServer/logs/` +* Application logs use standard JDK logging with info level by default + +== Further Resources + +* https://jakarta.ee/specifications/restful-ws/3.1/jakarta-restful-ws-spec-3.1.html[Jakarta RESTful Web Services Specification] +* https://openliberty.io/docs/latest/documentation.html[Open Liberty Documentation] +* https://download.eclipse.org/microprofile/microprofile-6.1/microprofile-spec-6.1.html[MicroProfile 6.1 Specification] \ No newline at end of file diff --git a/code/chapter11/user/pom.xml b/code/chapter11/user/pom.xml new file mode 100644 index 00000000..d2a17b45 --- /dev/null +++ b/code/chapter11/user/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + io.microprofile + user + 1.0-SNAPSHOT + war + + user-management + https://microprofile.io + + + UTF-8 + 21 + 21 + 10.0.0 + 7.1 + 25.0.0.1 + 1.18.36 + + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakarta.jakartaee-api.version} + provided + + + + org.eclipse.microprofile + microprofile + ${microprofile.version} + pom + provided + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + user + + + + io.openliberty.tools + liberty-maven-plugin + 3.8.2 + + userServer + runnable + 120 + + /user + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-war-plugin + 3.3.2 + + false + + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java new file mode 100644 index 00000000..347a04d2 --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/UserApplication.java @@ -0,0 +1,12 @@ +package io.microprofile.tutorial.store.user; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * Application class to activate REST resources. + */ +@ApplicationPath("/api") +public class UserApplication extends Application { + // The resources will be automatically discovered +} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java new file mode 100644 index 00000000..c2fe3df0 --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/User.java @@ -0,0 +1,75 @@ +package io.microprofile.tutorial.store.user.entity; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * User entity for the microprofile tutorial store application. + * Represents a user in the system with their profile information. + * Uses in-memory storage with thread-safe operations. + * + * Key features: + * - Validated user information + * - Secure password storage (hashed) + * - Contact information validation + * + * Potential improvements: + * 1. Auditing fields: + * - createdAt: Timestamp for account creation + * - modifiedAt: Last modification timestamp + * - version: For optimistic locking in concurrent updates + * + * 2. Security enhancements: + * - passwordSalt: For more secure password hashing + * - lastPasswordChange: Track password updates + * - failedLoginAttempts: For account security + * - accountLocked: Boolean for account status + * - lockTimeout: Timestamp for temporary locks + * + * 3. Additional features: + * - userRole: ENUM for role-based access (USER, ADMIN, etc.) + * - status: ENUM for account state (ACTIVE, INACTIVE, SUSPENDED) + * - emailVerified: Boolean for email verification + * - timeZone: User's preferred timezone + * - locale: User's preferred language/region + * - lastLoginAt: Track user activity + * + * 4. Compliance: + * - privacyPolicyAccepted: Track user consent + * - marketingPreferences: User communication preferences + * - dataRetentionPolicy: For GDPR compliance + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + + private Long userId; + + @NotEmpty(message = "Name cannot be empty") + @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") + private String name; + + @NotEmpty(message = "Email cannot be empty") + @Email(message = "Email should be valid") + @Size(max = 255, message = "Email must not exceed 255 characters") + private String email; + + @NotEmpty(message = "Password hash cannot be empty") + private String passwordHash; + + @Size(max = 200, message = "Address must not exceed 200 characters") + private String address; + + @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Phone number must be in E.164 format") + @Size(max = 15, message = "Phone number must not exceed 15 characters") + private String phoneNumber; +} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java new file mode 100644 index 00000000..e240f3ab --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/entity/package-info.java @@ -0,0 +1,6 @@ +/** + * Entity classes for the user management module. + * + * This package contains the domain objects representing user data. + */ +package io.microprofile.tutorial.store.user.entity; diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java new file mode 100644 index 00000000..a92fafcf --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/package-info.java @@ -0,0 +1,6 @@ +/** + * User management package for the microprofile tutorial store application. + * + * This package contains classes related to user management functionality. + */ +package io.microprofile.tutorial.store.user; \ No newline at end of file diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java new file mode 100644 index 00000000..db979c0c --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/UserRepository.java @@ -0,0 +1,135 @@ +package io.microprofile.tutorial.store.user.repository; + +import io.microprofile.tutorial.store.user.entity.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Thread-safe in-memory repository for User objects. + * This class provides CRUD operations for User entities using a ConcurrentHashMap for thread-safe storage + * and AtomicLong for safe ID generation in a concurrent environment. + * + * Key features: + * - Thread-safe operations using ConcurrentHashMap + * - Atomic ID generation + * - Immutable User objects in storage + * - Validation of user data + * - Optional return types for null-safety + * + * Note: This is a demo implementation. In production: + * - Consider using a persistent database + * - Add caching mechanisms + * - Implement proper pagination + * - Add audit logging + */ +@ApplicationScoped +public class UserRepository { + + private final Map users = new ConcurrentHashMap<>(); + private final AtomicLong nextId = new AtomicLong(1); + + /** + * Saves a user to the repository. + * If the user has no ID, a new ID is assigned. + * + * @param user The user to save + * @return The saved user with ID assigned + */ + public User save(User user) { + if (user.getUserId() == null) { + user.setUserId(nextId.getAndIncrement()); + } + User savedUser = User.builder() + .userId(user.getUserId()) + .name(user.getName()) + .email(user.getEmail()) + .passwordHash(user.getPasswordHash()) + .address(user.getAddress()) + .phoneNumber(user.getPhoneNumber()) + .build(); + users.put(savedUser.getUserId(), savedUser); + return savedUser; + } + + /** + * Finds a user by ID. + * + * @param id The user ID + * @return An Optional containing the user if found, or empty if not found + */ + public Optional findById(Long id) { + return Optional.ofNullable(users.get(id)); + } + + /** + * Finds a user by email. + * + * @param email The user's email + * @return An Optional containing the user if found, or empty if not found + */ + public Optional findByEmail(String email) { + return users.values().stream() + .filter(user -> user.getEmail().equals(email)) + .findFirst(); + } + + /** + * Retrieves all users from the repository. + * + * @return A list of all users + */ + public List findAll() { + return new ArrayList<>(users.values()); + } + + /** + * Deletes a user by ID. + * + * @param id The ID of the user to delete + * @return true if the user was deleted, false if not found + */ + public boolean deleteById(Long id) { + return users.remove(id) != null; + } + + /** + * Updates an existing user. + * + * @param id The ID of the user to update + * @param user The updated user information + * @return An Optional containing the updated user, or empty if not found + */ + /** + * Updates an existing user atomically. + * Only updates the user if it exists and the update is valid. + * + * @param id The ID of the user to update + * @param user The updated user information + * @return An Optional containing the updated user, or empty if not found + * @throws IllegalArgumentException if user is null or has invalid data + */ + public Optional update(Long id, User user) { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + return Optional.ofNullable(users.computeIfPresent(id, (key, existingUser) -> { + User updatedUser = User.builder() + .userId(id) + .name(user.getName() != null ? user.getName() : existingUser.getName()) + .email(user.getEmail() != null ? user.getEmail() : existingUser.getEmail()) + .passwordHash(user.getPasswordHash() != null ? user.getPasswordHash() : existingUser.getPasswordHash()) + .address(user.getAddress() != null ? user.getAddress() : existingUser.getAddress()) + .phoneNumber(user.getPhoneNumber() != null ? user.getPhoneNumber() : existingUser.getPhoneNumber()) + .build(); + return updatedUser; + })); + } +} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java new file mode 100644 index 00000000..0988dcb5 --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/repository/package-info.java @@ -0,0 +1,6 @@ +/** + * Repository classes for the user management module. + * + * This package contains classes responsible for data access and persistence. + */ +package io.microprofile.tutorial.store.user.repository; diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java new file mode 100644 index 00000000..bdd2e21b --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/UserResource.java @@ -0,0 +1,132 @@ +package io.microprofile.tutorial.store.user.resource; + +import io.microprofile.tutorial.store.user.entity.User; +import io.microprofile.tutorial.store.user.service.UserService; + +import java.net.URI; +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * REST resource for user management operations. + * Provides endpoints for creating, retrieving, updating, and deleting users. + * Implements standard RESTful practices with proper status codes and hypermedia links. + */ +@Path("/users") +@Tag(name = "User Management", description = "Operations for managing users") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class UserResource { + + @Inject + private UserService userService; + + @Context + private UriInfo uriInfo; + + @GET + @Operation(summary = "Get all users", description = "Returns a list of all users") + @APIResponse(responseCode = "200", description = "List of users") + @APIResponse(responseCode = "204", description = "No users found") + public Response getAllUsers() { + List users = userService.getAllUsers(); + + if (users.isEmpty()) { + return Response.noContent().build(); + } + + return Response.ok(users).build(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Get user by ID", description = "Returns a user by their ID") + @APIResponse(responseCode = "200", description = "User found") + @APIResponse(responseCode = "404", description = "User not found") + public Response getUserById( + @PathParam("id") + @Parameter(description = "User ID", required = true) + Long id) { + User user = userService.getUserById(id); + // Add HATEOAS links + URI selfLink = uriInfo.getBaseUriBuilder() + .path(UserResource.class) + .path(String.valueOf(user.getUserId())) + .build(); + return Response.ok(user) + .link(selfLink, "self") + .build(); + } + + @POST + @Operation(summary = "Create new user", description = "Creates a new user") + @APIResponse(responseCode = "201", description = "User created successfully") + @APIResponse(responseCode = "400", description = "Invalid user data") + @APIResponse(responseCode = "409", description = "Email already in use") + public Response createUser( + @Valid + @NotNull(message = "Request body cannot be empty") + @Parameter(description = "User to create", required = true) + User user) { + User createdUser = userService.createUser(user); + URI location = uriInfo.getAbsolutePathBuilder() + .path(String.valueOf(createdUser.getUserId())) + .build(); + return Response.created(location) + .entity(createdUser) + .build(); + } + + @PUT + @Path("/{id}") + @Operation(summary = "Update user", description = "Updates an existing user") + @APIResponse(responseCode = "200", description = "User updated successfully") + @APIResponse(responseCode = "400", description = "Invalid user data") + @APIResponse(responseCode = "404", description = "User not found") + @APIResponse(responseCode = "409", description = "Email already in use") + public Response updateUser( + @PathParam("id") + @Parameter(description = "User ID", required = true) + Long id, + + @Valid + @NotNull(message = "Request body cannot be empty") + @Parameter(description = "Updated user information", required = true) + User user) { + User updatedUser = userService.updateUser(id, user); + return Response.ok(updatedUser).build(); + } + + @DELETE + @Path("/{id}") + @Operation(summary = "Delete user", description = "Deletes a user by ID") + @APIResponse(responseCode = "204", description = "User successfully deleted") + @APIResponse(responseCode = "404", description = "User not found") + public Response deleteUser( + @PathParam("id") + @Parameter(description = "User ID to delete", required = true) + Long id) { + userService.deleteUser(id); + return Response.noContent().build(); + } +} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/resource/package-info.java new file mode 100644 index 00000000..e69de29b diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java new file mode 100644 index 00000000..db81d5e5 --- /dev/null +++ b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/UserService.java @@ -0,0 +1,130 @@ +package io.microprofile.tutorial.store.user.service; + +import io.microprofile.tutorial.store.user.entity.User; +import io.microprofile.tutorial.store.user.repository.UserRepository; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; + +/** + * Service class for User management operations. + */ +@ApplicationScoped +public class UserService { + + @Inject + private UserRepository userRepository; + + /** + * Creates a new user. + * + * @param user The user to create + * @return The created user + * @throws WebApplicationException if a user with the email already exists + */ + public User createUser(User user) { + // Check if email already exists + Optional existingUser = userRepository.findByEmail(user.getEmail()); + if (existingUser.isPresent()) { + throw new WebApplicationException("Email already in use", Response.Status.CONFLICT); + } + + // Hash the password + if (user.getPasswordHash() != null) { + user.setPasswordHash(hashPassword(user.getPasswordHash())); + } + + return userRepository.save(user); + } + + /** + * Gets a user by ID. + * + * @param id The user ID + * @return The user + * @throws WebApplicationException if the user is not found + */ + public User getUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new WebApplicationException("User not found", Response.Status.NOT_FOUND)); + } + + /** + * Gets all users. + * + * @return A list of all users + */ + public List getAllUsers() { + return userRepository.findAll(); + } + + /** + * Updates a user. + * + * @param id The user ID + * @param user The updated user information + * @return The updated user + * @throws WebApplicationException if the user is not found or if updating to an email that's already in use + */ + public User updateUser(Long id, User user) { + // Check if email already exists and belongs to another user + Optional existingUserWithEmail = userRepository.findByEmail(user.getEmail()); + if (existingUserWithEmail.isPresent() && !existingUserWithEmail.get().getUserId().equals(id)) { + throw new WebApplicationException("Email already in use", Response.Status.CONFLICT); + } + + // Hash the password if it has changed + if (user.getPasswordHash() != null && + !user.getPasswordHash().matches("^[a-fA-F0-9]{64}$")) { // Simple check if it's already a SHA-256 hash + user.setPasswordHash(hashPassword(user.getPasswordHash())); + } + + return userRepository.update(id, user) + .orElseThrow(() -> new WebApplicationException("User not found", Response.Status.NOT_FOUND)); + } + + /** + * Deletes a user. + * + * @param id The user ID + * @throws WebApplicationException if the user is not found + */ + public void deleteUser(Long id) { + boolean deleted = userRepository.deleteById(id); + if (!deleted) { + throw new WebApplicationException("User not found", Response.Status.NOT_FOUND); + } + } + + /** + * Simple password hashing using SHA-256. + * Note: In a production environment, use a more secure hashing algorithm with salt + * + * @param password The password to hash + * @return The hashed password + */ + private String hashPassword(String password) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(password.getBytes()); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to hash password", e); + } + } +} diff --git a/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java b/code/chapter11/user/src/main/java/io/microprofile/tutorial/store/user/service/package-info.java new file mode 100644 index 00000000..e69de29b diff --git a/code/chapter11/user/src/main/webapp/index.html b/code/chapter11/user/src/main/webapp/index.html new file mode 100644 index 00000000..fdb15f44 --- /dev/null +++ b/code/chapter11/user/src/main/webapp/index.html @@ -0,0 +1,107 @@ + + + + User Management Service API + + + +

User Management Service API

+

This service provides RESTful endpoints for managing users in the MicroProfile REST application.

+ +

Available Endpoints

+ +
+
GET /api/users
+
Get all users
+
+ Response: 200 (List of users), 204 (No users found) +
+
+ +
+
GET /api/users/{id}
+
Get a specific user by ID
+
+ Response: 200 (User found), 404 (User not found) +
+
+ +
+
POST /api/users
+
Create a new user
+
+ Request Body: User JSON object
+ Response: 201 (User created), 400 (Invalid data), 409 (Email already in use) +
+
+ +
+
PUT /api/users/{id}
+
Update an existing user
+
+ Request Body: Updated User JSON object
+ Response: 200 (User updated), 400 (Invalid data), 404 (User not found), 409 (Email already in use) +
+
+ +
+
DELETE /api/users/{id}
+
Delete a user by ID
+
+ Response: 204 (User deleted), 404 (User not found) +
+
+ +

Features

+
    +
  • Full CRUD operations for user management
  • +
  • Input validation using Bean Validation
  • +
  • HATEOAS links for improved API discoverability
  • +
  • OpenAPI documentation annotations
  • +
  • Proper HTTP status codes and error handling
  • +
  • Email uniqueness validation
  • +
+ +

Example User JSON

+
+{
+    "userId": 1,
+    "email": "user@example.com",
+    "firstName": "John",
+    "lastName": "Doe"
+}
+    
+ +