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/` diff --git a/code/chapter11/catalog/pom.xml b/code/chapter11/catalog/pom.xml index 853bfdc1..8f1917b3 100644 --- a/code/chapter11/catalog/pom.xml +++ b/code/chapter11/catalog/pom.xml @@ -11,8 +11,8 @@ - 17 - 17 + 21 + 21 UTF-8 UTF-8 @@ -30,7 +30,7 @@ org.projectlombok lombok - 1.18.26 + 1.18.36 provided @@ -46,7 +46,7 @@ org.eclipse.microprofile microprofile - 6.1 + 7.1 pom provided @@ -55,6 +55,16 @@ ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + io.openliberty.tools 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/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 diff --git a/code/chapter11/inventory/pom.xml b/code/chapter11/inventory/pom.xml index dcf6eb2b..988b3fb3 100644 --- a/code/chapter11/inventory/pom.xml +++ b/code/chapter11/inventory/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 6050 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 index ae8a9416..3ee718c0 100644 --- 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 @@ -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/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/order/pom.xml b/code/chapter11/order/pom.xml index ff7fdc97..9e702791 100644 --- a/code/chapter11/order/pom.xml +++ b/code/chapter11/order/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/order/restart-server.sh b/code/chapter11/order/restart-server.sh old mode 100755 new mode 100644 diff --git a/code/chapter11/order/run-docker.sh b/code/chapter11/order/run-docker.sh old mode 100755 new mode 100644 diff --git a/code/chapter11/order/run.sh b/code/chapter11/order/run.sh old mode 100755 new mode 100644 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/payment/README.adoc b/code/chapter11/payment/README.adoc index 500d8075..2a580965 100644 --- a/code/chapter11/payment/README.adoc +++ b/code/chapter11/payment/README.adoc @@ -1,6 +1,6 @@ = 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. +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 @@ -38,8 +38,8 @@ This microservice is part of the Jakarta EE 10 and MicroProfile 6.1-based e-comm === Prerequisites -* JDK 17 or higher -* Maven 3.6.0 or higher +* JDK 21 or higher +* Maven 3.13.0 or higher === Local Development diff --git a/code/chapter11/payment/pom.xml b/code/chapter11/payment/pom.xml index 12b8fada..914f37e3 100644 --- a/code/chapter11/payment/pom.xml +++ b/code/chapter11/payment/pom.xml @@ -12,8 +12,8 @@ UTF-8 - 17 - 17 + 21 + 21 UTF-8 UTF-8 @@ -33,7 +33,7 @@ org.projectlombok lombok - 1.18.26 + 1.18.36 provided @@ -49,7 +49,7 @@ org.eclipse.microprofile microprofile - 6.1 + 7.1 pom provided @@ -65,6 +65,16 @@ ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + io.openliberty.tools diff --git a/code/chapter11/payment/run-docker.sh b/code/chapter11/payment/run-docker.sh old mode 100755 new mode 100644 diff --git a/code/chapter11/payment/run.sh b/code/chapter11/payment/run.sh old mode 100755 new mode 100644 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 index 5100206e..81576184 100644 --- 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 @@ -1,52 +1,67 @@ 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.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; 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(); +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; - return products; - } +/** + * 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 { - 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(); + /** + * Retrieves all products from the catalog service. + * + * @return List of all products + * @throws RuntimeException if service returns 5xx error + */ + @GET + List getAllProducts(); - 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 + /** + * 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/ProductClientJson.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java similarity index 97% rename from code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductClientJson.java rename to code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/client/ProductJakartaRestClient.java index 32333de3..095a448e 100644 --- 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/ProductJakartaRestClient.java @@ -7,7 +7,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -public class ProductClientJson { +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. 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/examples/ProductClientExample.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java similarity index 78% rename from code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/examples/ProductClientExample.java rename to code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/demo/ProductClientRunner.java index 0decaa3f..34963e2e 100644 --- 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/demo/ProductClientRunner.java @@ -1,24 +1,25 @@ -package io.microprofile.tutorial.store.payment.examples; +package io.microprofile.tutorial.store.payment.demo; -import io.microprofile.tutorial.store.payment.client.ProductClientJson; +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 ProductClientJson.getProductsWithJsonp method. + * Example demonstrating how to use the ProductJakartaRestClientSimple.getProductsWithJsonp method. */ -public class ProductClientExample { +public class ProductClientRunner { - private static final Logger LOGGER = Logger.getLogger(ProductClientExample.class.getName()); + 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 = ProductClientJson.getProductsWithJsonp(null); + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(null); printProducts("Default URL", products); } catch (Exception e) { LOGGER.warning("Failed to fetch products with default URL: " + e.getMessage()); @@ -28,7 +29,7 @@ public static void main(String[] args) { LOGGER.info("=== Example 2: Using custom catalog service URL ==="); try { String catalogUrl = "http://localhost:5050/catalog/api/products"; - Product[] products = ProductClientJson.getProductsWithJsonp(catalogUrl); + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(catalogUrl); printProducts("Custom catalog URL", products); } catch (Exception e) { LOGGER.warning("Failed to fetch products from catalog service: " + e.getMessage()); @@ -45,7 +46,7 @@ public static void main(String[] args) { for (String url : environmentUrls) { try { LOGGER.info("Trying URL: " + url); - Product[] products = ProductClientJson.getProductsWithJsonp(url); + Product[] products = ProductJakartaRestClientSimple.getProductsWithJsonp(url); printProducts("URL: " + url, products); break; // Stop on first successful call } catch (Exception e) { 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/PaymentProductResource.java b/code/chapter11/payment/src/main/java/io/microprofile/tutorial/store/payment/resource/PaymentProductResource.java index 9d1069ee..2927e7f3 100644 --- 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 @@ -1,6 +1,6 @@ package io.microprofile.tutorial.store.payment.resource; -import io.microprofile.tutorial.store.payment.client.ProductClientJson; +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; @@ -22,7 +22,7 @@ import java.util.logging.Logger; /** - * REST resource demonstrating how to use ProductClientJson.getProductsWithJsonp + * REST resource demonstrating how to use ProductJakartaRestClient.getProductsWithJsonp * within REST endpoints for the Payment service. */ @ApplicationScoped @@ -52,7 +52,7 @@ public Response getAllProducts() { LOGGER.info("REST: Fetching all products for payment processing"); try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); if (products != null) { LOGGER.info("Successfully retrieved " + products.length + " products"); @@ -95,7 +95,7 @@ public Response getProductsFromUrl( LOGGER.info("REST: Fetching products from URL: " + catalogUrl); try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogUrl); + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogUrl); Map result = new HashMap<>(); result.put("sourceUrl", catalogUrl); 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/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 index 884769eb..ba677ea3 100644 --- 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 @@ -1,6 +1,6 @@ package io.microprofile.tutorial.store.payment.service; -import io.microprofile.tutorial.store.payment.client.ProductClientJson; +import io.microprofile.tutorial.store.payment.client.ProductJakartaRestClient; import io.microprofile.tutorial.store.payment.dto.product.Product; import jakarta.enterprise.context.ApplicationScoped; @@ -67,7 +67,7 @@ public Product getProductDetails(Long productId) { LOGGER.info("Fetching product details for ID: " + productId); try { - Product[] allProducts = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); + Product[] allProducts = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); if (allProducts != null) { for (Product product : allProducts) { @@ -98,7 +98,7 @@ public List getProductsByPriceRange(double minPrice, double maxPrice) { LOGGER.info("Fetching products in price range: " + minPrice + " - " + maxPrice); try { - Product[] allProducts = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); + Product[] allProducts = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); if (allProducts == null) { LOGGER.warning("No products returned from catalog service"); @@ -129,7 +129,7 @@ public Product[] getAllProducts() { LOGGER.info("Fetching all products"); try { - Product[] products = ProductClientJson.getProductsWithJsonp(catalogServiceUrl); + Product[] products = ProductJakartaRestClient.getProductsWithJsonp(catalogServiceUrl); if (products != null) { LOGGER.info("Retrieved " + products.length + " products"); 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 index cf834366..93c66f76 100644 --- a/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties +++ b/code/chapter11/payment/src/main/resources/META-INF/microprofile-config.properties @@ -1,5 +1,5 @@ # microprofile-config.properties -mp.openapi.scan=true +mp.openapi.scan.disable=false product.maintenanceMode=false # Product Service Configuration @@ -8,4 +8,28 @@ 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 +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/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/run-all-services.sh b/code/chapter11/run-all-services.sh old mode 100755 new mode 100644 index 51277204..7a63b901 --- 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" \ No newline at end of file diff --git a/code/chapter11/shipment/pom.xml b/code/chapter11/shipment/pom.xml index 9a78242c..d5e31f65 100644 --- a/code/chapter11/shipment/pom.xml +++ b/code/chapter11/shipment/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/shipment/run-docker.sh b/code/chapter11/shipment/run-docker.sh old mode 100755 new mode 100644 diff --git a/code/chapter11/shipment/run.sh b/code/chapter11/shipment/run.sh old mode 100755 new mode 100644 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/shoppingcart/pom.xml b/code/chapter11/shoppingcart/pom.xml index 9451fea0..df9eae09 100644 --- a/code/chapter11/shoppingcart/pom.xml +++ b/code/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/shoppingcart/run-docker.sh old mode 100755 new mode 100644 diff --git a/code/chapter11/shoppingcart/run.sh b/code/chapter11/shoppingcart/run.sh old mode 100755 new mode 100644 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/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/pom.xml b/code/chapter11/user/pom.xml index f743ec40..d2a17b45 100644 --- a/code/chapter11/user/pom.xml +++ b/code/chapter11/user/pom.xml @@ -14,12 +14,12 @@ UTF-8 - 17 - 17 + 21 + 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/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