diff --git a/.github/workflows/build-and-deploy-tutorial.yml b/.github/workflows/build-and-deploy-tutorial.yml index 9e80a9ea..28f11029 100644 --- a/.github/workflows/build-and-deploy-tutorial.yml +++ b/.github/workflows/build-and-deploy-tutorial.yml @@ -56,10 +56,11 @@ jobs: bundler-cache: false # Install Antora CLI, site generator, and advanced extensions + # Versions are pinned to avoid breakage from alpha/pre-release updates - name: Install Antora and Extensions run: | - npm install @antora/cli @antora/site-generator-default - npm install @antora/lunr-extension @antora/pdf-extension @asciidoctor/tabs + npm install @antora/cli@3.1.10 @antora/site-generator-default@3.1.10 + npm install "@antora/lunr-extension@1.0.0-alpha.10" "@antora/pdf-extension@1.0.0-alpha.11" "@asciidoctor/tabs@1.0.0-beta.6" gem install asciidoctor-pdf asciidoctor-kroki asciidoctor-plantuml # Make update-repo-url.sh executable and run it to update URLs @@ -132,12 +133,13 @@ jobs: bundler-cache: false # Install Antora CLI, site generator, and advanced extensions + # Versions are pinned to avoid breakage from alpha/pre-release updates - name: Install Antora and Extensions run: | - npm install @antora/cli @antora/site-generator-default - npm install @antora/lunr-extension @antora/pdf-extension @asciidoctor/tabs + npm install @antora/cli@3.1.10 @antora/site-generator-default@3.1.10 + npm install "@antora/lunr-extension@1.0.0-alpha.10" "@antora/pdf-extension@1.0.0-alpha.11" "@asciidoctor/tabs@1.0.0-beta.6" gem install asciidoctor-pdf asciidoctor-kroki asciidoctor-plantuml - + # Verify that Antora was installed correctly (useful for debugging) - name: Verify Antora Installation run: npm list @antora/cli @antora/site-generator-default || echo "Antora packages are not installed." diff --git a/code/.gitignore b/code/.gitignore index 66b3aae3..7f02d758 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -39,7 +39,6 @@ buildNumber.properties .liberty/ .factorypath wlp/ -liberty/ # IntelliJ IDEA .idea/ diff --git a/code/chapter11/README.adoc b/code/chapter11/README.adoc index ce07afb5..752c3287 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,43 +62,24 @@ 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: - -==== User Service -[source,bash] ----- -cd user -mvn liberty:run ----- -The service will be available at http://localhost:6050/user - -==== Inventory Service -[source,bash] ----- -cd inventory -mvn liberty:run ----- -The service will be available at http://localhost:7050/inventory +2. Start each microservice individually (each command is run from `code/chapter11/`): -==== Order Service [source,bash] ---- -cd order -mvn liberty:run +cd user && mvn liberty:run # http://localhost:6050/user +cd inventory && mvn liberty:run # http://localhost:7050/inventory +cd order && mvn liberty:run # http://localhost:8050/order +cd catalog && mvn liberty:run # http://localhost:5050/catalog +cd payment && mvn liberty:run # http://localhost:9050/payment +cd shoppingcart && mvn liberty:run # http://localhost:4050/shoppingcart +cd shipment && mvn liberty:run # http://localhost:8060/shipment ---- -The service will be available at http://localhost:8050/order -==== Catalog Service -[source,bash] ----- -cd catalog -mvn liberty:run ----- -The service will be available at http://localhost:9050/catalog +See each service's own `README.adoc` for detailed build, run, and testing instructions. === Building the Application @@ -133,10 +108,9 @@ Or manually: [source,bash] ---- # Build all projects first -cd user && mvn clean package && cd .. -cd inventory && mvn clean package && cd .. -cd order && mvn clean package && cd .. -cd catalog && mvn clean package && cd .. +for svc in user inventory order catalog payment shoppingcart shipment; do + (cd "$svc" && mvn clean package) +done # Start all services docker-compose up -d @@ -147,155 +121,46 @@ This will start all services in Docker containers with the following endpoints: * User Service: http://localhost:6050/user * Inventory Service: http://localhost:7050/inventory * Order Service: http://localhost:8050/order -* Catalog Service: http://localhost:9050/catalog +* Catalog Service: http://localhost:5050/catalog +* Payment Service: http://localhost:9050/payment +* Shopping Cart Service: http://localhost:4050/shoppingcart +* Shipment Service: http://localhost:8060/shipment == API Documentation -Each microservice provides its own OpenAPI documentation, available at: - -* User Service: http://localhost:6050/user/openapi -* Inventory Service: http://localhost:7050/inventory/openapi -* Order Service: http://localhost:8050/order/openapi -* Catalog Service: http://localhost:9050/catalog/openapi - -== Testing the Services - -=== User Service - -[source,bash] ----- -# Get all users -curl -X GET http://localhost:6050/user/api/users - -# Create a new user -curl -X POST http://localhost:6050/user/api/users \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Jane Doe", - "email": "jane@example.com", - "passwordHash": "password123", - "address": "123 Main St", - "phoneNumber": "555-123-4567" - }' - -# Get a user by ID -curl -X GET http://localhost:6050/user/api/users/1 - -# Update a user -curl -X PUT http://localhost:6050/user/api/users/1 \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Jane Smith", - "email": "jane@example.com", - "passwordHash": "password123", - "address": "456 Oak Ave", - "phoneNumber": "555-123-4567" - }' - -# Delete a user -curl -X DELETE http://localhost:6050/user/api/users/1 ----- +Each microservice provides its own OpenAPI documentation at the server root (not under the service's context root): -=== Inventory Service +* User Service: http://localhost:6050/openapi +* Inventory Service: http://localhost:7050/openapi +* Order Service: http://localhost:8050/openapi +* Catalog Service: http://localhost:5050/openapi +* Payment Service: http://localhost:9050/openapi +* Shopping Cart Service: http://localhost:4050/openapi +* Shipment Service: http://localhost:8060/openapi -[source,bash] ----- -# Get all inventory items -curl -X GET http://localhost:7050/inventory/api/inventories - -# Create a new inventory item -curl -X POST http://localhost:7050/inventory/api/inventories \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 101, - "quantity": 25 - }' - -# Get inventory by ID -curl -X GET http://localhost:7050/inventory/api/inventories/1 - -# Get inventory by product ID -curl -X GET http://localhost:7050/inventory/api/inventories/product/101 - -# Update inventory -curl -X PUT http://localhost:7050/inventory/api/inventories/1 \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 101, - "quantity": 50 - }' - -# Update product quantity -curl -X PATCH http://localhost:7050/inventory/api/inventories/product/101/quantity/75 - -# Delete inventory -curl -X DELETE http://localhost:7050/inventory/api/inventories/1 ----- - -=== Order Service - -[source,bash] ----- -# Get all orders -curl -X GET http://localhost:8050/order/api/orders - -# Create a new order -curl -X POST http://localhost:8050/order/api/orders \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "CREATED", - "orderItems": [ - { - "productId": 101, - "quantity": 2, - "priceAtOrder": 49.99 - }, - { - "productId": 102, - "quantity": 1, - "priceAtOrder": 50.00 - } - ] - }' - -# Get order by ID -curl -X GET http://localhost:8050/order/api/orders/1 - -# Update order status -curl -X PATCH http://localhost:8050/order/api/orders/1/status/PAID - -# Get items for an order -curl -X GET http://localhost:8050/order/api/orders/1/items - -# Delete order -curl -X DELETE http://localhost:8050/order/api/orders/1 ----- +== MicroProfile Rest Client 4.0 Features -=== Catalog Service +Several services demonstrate MicroProfile Rest Client 4.0: -[source,bash] ----- -# Get all products -curl -X GET http://localhost:9050/catalog/api/products +* *Inventory Service* showcases the three core usage patterns — CDI-injected `@RestClient`, programmatic `RestClientBuilder` (including the 4.0 `baseUri(String)` convenience method), and `AutoCloseable` client interfaces. See `inventory/README.adoc` for the full walkthrough and code samples. +* *Payment Service* builds on this with `ResponseExceptionMapper` custom error handling, `ClientRequestFilter`/`ClientResponseFilter` cross-cutting concerns, and additional `RestClientBuilder` scenarios (dynamic/environment-specific configuration, batch processing). See `payment/README.adoc` for the full set of examples. -# Get a product by ID -curl -X GET http://localhost:9050/catalog/api/products/1 +== Testing the Services -# Search products -curl -X GET "http://localhost:9050/catalog/api/products/search?keyword=laptop" ----- +Each service's own `README.adoc` (`user`, `inventory`, `order`, `catalog`, `payment`, `shipment`, `shoppingcart`) documents its full endpoint list and `curl` examples for testing — refer to those for up-to-date, service-specific instructions rather than duplicating them here. In particular, `payment/README.adoc` includes a comprehensive, step-by-step walkthrough of the MicroProfile Rest Client 4.0 patterns (injected `@RestClient`, `RestClientBuilder`, `ResponseExceptionMapper`, custom filters) demonstrated against the Catalog Service. == Project Structure [source] ---- -liberty-rest-app/ +chapter11/ ├── user/ # User management service ├── inventory/ # Inventory management service ├── order/ # Order management service -└── catalog/ # Product catalog service +├── catalog/ # Product catalog service +├── payment/ # Payment processing service +├── shoppingcart/ # Shopping cart service +└── shipment/ # Shipment tracking service ---- Each service follows a similar internal structure: @@ -308,7 +173,7 @@ service/ │ │ ├── java/ # Java source code │ │ ├── liberty/ # Liberty server configuration │ │ └── webapp/ # Web resources -│ └── test/ # Test code +│ └── test/ # Test code (where present) └── pom.xml # Maven configuration ---- @@ -320,13 +185,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/Dockerfile b/code/chapter11/catalog/Dockerfile new file mode 100644 index 00000000..537c9861 --- /dev/null +++ b/code/chapter11/catalog/Dockerfile @@ -0,0 +1,11 @@ +FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi + +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +COPY --chown=1001:0 target/catalog.war /config/apps/ + +RUN configure.sh + +EXPOSE 5050 5051 + +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/catalog/README.adoc b/code/chapter11/catalog/README.adoc index cffee0be..77fe2b54 100644 --- a/code/chapter11/catalog/README.adoc +++ b/code/chapter11/catalog/README.adoc @@ -9,7 +9,7 @@ toc::[] == Overview -The MicroProfile Catalog Service is a modern Jakarta EE 10 application built with MicroProfile 6.1 specifications and running on Open Liberty. This service provides a RESTful API for product catalog management with enhanced MicroProfile features. +The MicroProfile Catalog Service is a modern Jakarta EE 10 application built with MicroProfile 7.1 specifications and running on Open Liberty. This service provides a RESTful API for product catalog management with enhanced MicroProfile features. This project demonstrates the key capabilities of MicroProfile OpenAPI and in-memory persistence architecture. @@ -161,13 +161,11 @@ mp.openapi.scan=true # Maintenance mode configuration product.maintenanceMode=false -product.maintenanceMessage=The product catalog service is currently in maintenance mode. Please try again later. ---- The maintenance mode configuration allows dynamic control of service availability: * `product.maintenanceMode` - When set to `true`, the service returns a 503 Service Unavailable response -* `product.maintenanceMessage` - Customizable message displayed when the service is in maintenance mode ==== Maintenance Mode Implementation @@ -178,17 +176,12 @@ The service checks the maintenance mode configuration before processing requests @Inject @ConfigProperty(name="product.maintenanceMode", defaultValue="false") private boolean maintenanceMode; - -@Inject -@ConfigProperty(name="product.maintenanceMessage", - defaultValue="The product catalog service is currently in maintenance mode. Please try again later.") -private String maintenanceMessage; // In request handling method -if (maintenance.isMaintenanceMode()) { +if (maintenanceMode) { return Response .status(Response.Status.SERVICE_UNAVAILABLE) - .entity(maintenance.getMaintenanceMessage()) + .entity("Service is under maintenance") .build(); } ---- @@ -199,6 +192,8 @@ This pattern enables: * Dynamic control without redeployment (when using external configuration sources) * Clear communication to API consumers +NOTE: The maintenance message is currently a fixed string in `ProductResource`. There is no `product.maintenanceMessage` configuration property; customizing the message requires a code change. + == Architecture The application follows a layered architecture pattern: @@ -243,7 +238,7 @@ mvn clean package docker build -t catalog-service . # Run the container -docker run -d -p 5050:5050 --name catalog-service catalog-service +docker run -d -p 5050:5050 -p 5051:5051 --name catalog-service catalog-service ---- ==== AtomicLong in Containerized Environments @@ -299,7 +294,9 @@ catalog/ │ │ │ └── io/microprofile/tutorial/store/ │ │ │ └── product/ │ │ │ ├── entity/ # Domain entities +│ │ │ ├── repository/ # In-memory data access layer │ │ │ ├── resource/ # REST resources +│ │ │ ├── service/ # Business logic │ │ │ └── ProductRestApplication.java │ │ ├── liberty/ │ │ │ └── config/ @@ -311,7 +308,6 @@ catalog/ │ │ ├── index.html # Landing page with API documentation │ │ └── WEB-INF/ │ │ └── web.xml # Web application configuration -│ └── test/ # Test classes └── pom.xml # Maven build file ---- @@ -329,8 +325,8 @@ To build and run the application: [source,bash] ---- # Clone the repository -git clone https://github.com/yourusername/liberty-rest-app.git -cd code/catalog +git clone https://github.com/microprofile/microprofile-tutorial.git +cd code/chapter11/catalog # Build the application mvn clean package @@ -349,7 +345,7 @@ mvn liberty:run curl -X GET http://localhost:5050/openapi # Check if service is in maintenance mode -curl -X GET http://localhost:5050/api/products +curl -X GET http://localhost:5050/catalog/api/products ---- To view the Swagger UI, open the following URL in your browser: @@ -364,18 +360,24 @@ The application uses the following Liberty server configuration: [source,xml] ---- - + + + microProfile-7.1 jakartaEE-10.0 - microProfile-6.1 restfulWS jsonp jsonb cdi mpConfig mpOpenAPI + mpHealth + mpMetrics + mpTelemetry + mpFaultTolerance + mpJwt + mpRestClient - @@ -393,22 +395,27 @@ To add a new endpoint: 3. Add OpenAPI annotations for documentation 4. Implement the business logic -Example: +The service already follows this pattern for its `/search` endpoint, which delegates the actual +filtering to `ProductService`/`ProductRepository` rather than filtering in the resource class: [source,java] ---- @GET @Path("/search") @Produces(MediaType.APPLICATION_JSON) -@Operation(summary = "Search products", description = "Search products by name") +@Operation(summary = "Search products", description = "Search products by criteria") @APIResponses({ - @APIResponse(responseCode = "200", description = "Products matching search criteria") + @APIResponse(responseCode = "200", description = "Search results", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = Product.class))) }) -public Response searchProducts(@QueryParam("name") String name) { - List matchingProducts = products.stream() - .filter(p -> p.getName().toLowerCase().contains(name.toLowerCase())) - .collect(Collectors.toList()); - return Response.ok(matchingProducts).build(); +public Response searchProducts( + @QueryParam("name") String name, + @QueryParam("description") String description, + @QueryParam("minPrice") Double minPrice, + @QueryParam("maxPrice") Double maxPrice) { + List results = productService.searchProducts(name, description, minPrice, maxPrice); + return Response.ok(results).build(); } ---- @@ -495,8 +502,6 @@ public Product createProduct(Product product) { This implementation ensures ID integrity while supporting both system-generated and client-provided IDs. -This enables scanning of OpenAPI annotations in the application. - == Troubleshooting === Common Issues @@ -505,8 +510,6 @@ This enables scanning of OpenAPI annotations in the application. * *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations * *Service always in maintenance mode*: Check the `product.maintenanceMode` property in `microprofile-config.properties` * *API returning 503 responses*: The service is likely in maintenance mode; set `product.maintenanceMode=false` in configuration -* *OpenAPI documentation not available*: Make sure `mp.openapi.scan=true` is set in the properties file -* *Concurrent modification exceptions*: Ensure proper use of thread-safe collections and operations === Thread Safety Troubleshooting @@ -579,7 +582,7 @@ Server logs can be found at: [source] ---- -target/liberty/wlp/usr/servers/defaultServer/logs/ +target/liberty/wlp/usr/servers/mpServer/logs/ ---- == Resources @@ -597,7 +600,7 @@ The application includes a user-friendly HTML landing page (`index.html`) that p ==== Maintenance Mode Configuration in the UI -The index.html page is designed to work seamlessly with the maintenance mode configuration. When maintenance mode is enabled via the `product.maintenanceMode` property, all API endpoints return a 503 Service Unavailable response with the configured maintenance message. +The index.html page is designed to work seamlessly with the maintenance mode configuration. When maintenance mode is enabled via the `product.maintenanceMode` property, all API endpoints return a 503 Service Unavailable response with a fixed maintenance message. The landing page displays comprehensive documentation about the API regardless of the maintenance state, allowing developers to continue learning about the API even when the service is undergoing maintenance. 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 index 8fec0a65..69d6375f 100644 --- a/code/chapter11/catalog/src/main/liberty/config/server.xml +++ b/code/chapter11/catalog/src/main/liberty/config/server.xml @@ -2,8 +2,8 @@ + microProfile-7.1 jakartaEE-10.0 - microProfile-6.1 restfulWS jsonp jsonb diff --git a/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties b/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties index 03fbb4d9..532979e9 100644 --- a/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties +++ b/code/chapter11/catalog/src/main/resources/META-INF/microprofile-config.properties @@ -1,5 +1,5 @@ # microprofile-config.properties -product.maintainenceMode=false +product.maintenanceMode=false # Enable OpenAPI scanning mp.openapi.scan=true \ 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/Dockerfile b/code/chapter11/inventory/Dockerfile new file mode 100644 index 00000000..c0f507be --- /dev/null +++ b/code/chapter11/inventory/Dockerfile @@ -0,0 +1,11 @@ +FROM icr.io/appcafe/open-liberty:full-java17-openj9-ubi + +COPY --chown=1001:0 src/main/liberty/config/ /config/ + +COPY --chown=1001:0 target/inventory.war /config/apps/ + +RUN configure.sh + +EXPOSE 7050 7051 + +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/inventory/README.adoc b/code/chapter11/inventory/README.adoc index 2df7fec7..477b50fa 100644 --- a/code/chapter11/inventory/README.adoc +++ b/code/chapter11/inventory/README.adoc @@ -7,7 +7,7 @@ A comprehensive Jakarta EE and MicroProfile-based REST service for inventory man == Overview -The Inventory Service is a production-ready microservice built with Jakarta EE 10.0 and MicroProfile 6.1, showcasing comprehensive REST client integration patterns with the Catalog Service. This service demonstrates three different approaches to MicroProfile Rest Client usage: +The Inventory Service is a microservice built with Jakarta EE 10.0 and MicroProfile 7.1, showcasing comprehensive REST client integration patterns with the Catalog Service. This service demonstrates three different approaches to MicroProfile Rest Client usage: * **Injected REST Client** (`@RestClient`) for standard operations * **RestClientBuilder** with custom timeouts for availability checks @@ -35,7 +35,6 @@ The Inventory Service is a production-ready microservice built with Jakarta EE 1 * Comprehensive exception handling * Transaction management for atomic operations * OpenAPI documentation with Swagger UI -* Health checks and service monitoring == Running the Application @@ -245,28 +244,26 @@ curl -X POST http://localhost:7050/inventory/api/inventories/bulk \ [source,bash] ---- -# Filter by location -curl -X GET "http://localhost:6050/inventory/api/inventories?location=Warehouse%20A" - # Filter by minimum quantity -curl -X GET "http://localhost:6050/inventory/api/inventories?minQuantity=10" +curl -X GET "http://localhost:7050/inventory/api/inventories?minQuantity=10" + +# Filter by maximum quantity +curl -X GET "http://localhost:7050/inventory/api/inventories?maxQuantity=100" + +# Paginate results +curl -X GET "http://localhost:7050/inventory/api/inventories?page=0&size=10" ---- -== Test Scripts +== Automated Tests -Comprehensive test scripts are available to test all functionality: +JUnit tests are provided under `src/test/java`: -* **`test-inventory-endpoints.sh`** - Complete test suite covering all endpoints and MicroProfile Rest Client features -* **`quick-test-commands.sh`** - Quick reference commands for manual testing -* **`TEST-SCRIPTS-README.md`** - Detailed documentation of test scenarios and expected responses +* **`InventoryServiceTest`** - Unit tests for `InventoryService` business logic +* **`InventoryServiceIntegrationTest`** - Integration-style tests exercising the service layer [source,bash] ---- -# Run comprehensive test suite -./test-inventory-endpoints.sh - -# View test documentation -cat TEST-SCRIPTS-README.md +mvn test ---- == Configuration @@ -284,22 +281,30 @@ product-service/mp-rest/readTimeout=10000 product-service/mp-rest/followRedirects=true ---- -**RestClientBuilder Configuration** (programmatic): +**RestClientBuilder Configuration** (programmatic, using the MP Rest Client 4.0 `baseUri(String)` convenience method): [source,java] ---- -# Availability check client (5s/10s timeout) -URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); +// Availability check client (5s/10s timeout) +RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build(ProductServiceClient.class); -# Product info client (3s/8s timeout) -URI catalogServiceUri = URI.create("http://localhost:5050/catalog/api"); +// Product info client (3s/8s timeout) +RestClientBuilder.newBuilder() + .baseUri("http://localhost:5050/catalog/api") + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(8, TimeUnit.SECONDS) + .build(ProductServiceClient.class); ---- == OpenAPI Documentation View the complete API documentation: -* **Swagger UI**: http://localhost:7050/inventory/api/openapi-ui/ -* **OpenAPI JSON**: http://localhost:7050/inventory/api/openapi +* **Swagger UI**: http://localhost:7050/openapi/ui +* **OpenAPI JSON**: http://localhost:7050/openapi * **Service Landing Page**: http://localhost:7050/inventory/ == Project Structure @@ -308,16 +313,23 @@ View the complete API documentation: ---- inventory/ ├── src/ -│ └── main/ -│ ├── java/ # Java source files -│ │ └── io/microprofile/tutorial/store/inventory/ -│ │ ├── entity/ # Domain entities -│ │ ├── exception/ # Custom exceptions -│ │ ├── service/ # Business logic -│ │ └── resource/ # REST endpoints -│ ├── liberty/ -│ │ └── config/ # Liberty server configuration -│ └── webapp/ # Web resources +│ ├── main/ +│ │ ├── java/ # Java source files +│ │ │ └── io/microprofile/tutorial/store/inventory/ +│ │ │ ├── client/ # MicroProfile Rest Client interface +│ │ │ ├── dto/ # Data transfer objects +│ │ │ ├── entity/ # Domain entities +│ │ │ ├── exception/ # Custom exceptions +│ │ │ ├── repository/ # In-memory data access layer +│ │ │ ├── service/ # Business logic +│ │ │ └── resource/ # REST endpoints +│ │ ├── liberty/ +│ │ │ └── config/ # Liberty server configuration +│ │ ├── resources/ +│ │ │ └── META-INF/ +│ │ │ └── microprofile-config.properties # REST client configuration +│ │ └── webapp/ # Web resources +│ └── test/ # Unit and integration tests └── pom.xml # Project dependencies and build ---- diff --git a/code/chapter11/inventory/pom.xml b/code/chapter11/inventory/pom.xml index dcf6eb2b..f8ff0d45 100644 --- a/code/chapter11/inventory/pom.xml +++ b/code/chapter11/inventory/pom.xml @@ -14,15 +14,15 @@ 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 - 6051 + 7050 + 7051 inventory diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java index 32189b02..e7a5ceaa 100644 --- a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java +++ b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/resource/InventoryResource.java @@ -3,6 +3,7 @@ import io.microprofile.tutorial.store.inventory.entity.Inventory; import io.microprofile.tutorial.store.inventory.service.InventoryService; import io.microprofile.tutorial.store.inventory.dto.Product; +import io.microprofile.tutorial.store.inventory.dto.InventoryWithProductInfo; import java.net.URI; import java.util.List; @@ -263,4 +264,48 @@ public Response getProductInfo( .build(); } } + + @GET + @Path("/{id}/with-product-info") + @Operation(summary = "Get inventory enriched with product information", + description = "Returns an inventory item along with product details from the catalog service") + @APIResponse( + responseCode = "200", + description = "Inventory item enriched with product information", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = InventoryWithProductInfo.class) + ) + ) + @APIResponse( + responseCode = "404", + description = "Inventory not found" + ) + public InventoryWithProductInfo getInventoryWithProductInfo( + @Parameter(description = "ID of the inventory item", required = true) + @PathParam("id") Long id) { + return inventoryService.getInventoryWithProductInfo(id); + } + + @POST + @Path("/bulk") + @Operation(summary = "Bulk create inventory items", description = "Creates multiple inventory items in a single request, validating each against the catalog service") + @APIResponse( + responseCode = "201", + description = "Inventory items created", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = Inventory.class) + ) + ) + @APIResponse( + responseCode = "409", + description = "Inventory for one or more products already exists" + ) + public Response createBulkInventories( + @Parameter(description = "Inventory items to create", required = true) + @NotNull List<@Valid Inventory> inventories) { + List created = inventoryService.createBulkInventories(inventories); + return Response.status(Response.Status.CREATED).entity(created).build(); + } } diff --git a/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java b/code/chapter11/inventory/src/main/java/io/microprofile/tutorial/store/inventory/service/InventoryService.java 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 index 1bc1d1f7..249a72e1 100644 --- a/code/chapter11/inventory/src/main/liberty/config/server.xml +++ b/code/chapter11/inventory/src/main/liberty/config/server.xml @@ -1,15 +1,20 @@ - + - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 + microProfile-7.1 + jakartaEE-10.0 + restfulWS + jsonp + jsonb + cdi + beanValidation + com.ibm.websphere.appserver.transaction-2.0 + mpOpenAPI + mpRestClient - \ No newline at end of file + diff --git a/code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties b/code/chapter11/inventory/src/main/resources/META-INF/microprofile-config.properties similarity index 100% rename from code/chapter11/inventory/src/main/webapp/META-INF/microprofile-config.properties rename to code/chapter11/inventory/src/main/resources/META-INF/microprofile-config.properties diff --git a/code/chapter11/order/README.adoc b/code/chapter11/order/README.adoc new file mode 100644 index 00000000..f0b67921 --- /dev/null +++ b/code/chapter11/order/README.adoc @@ -0,0 +1,217 @@ += Order Service +:toc: left +:icons: font +:source-highlighter: highlightjs + +A Jakarta EE and MicroProfile-based REST service for order management in the Liberty Rest App demo. + +== Features + +* Provides CRUD operations for order management +* Tracks orders with order_id, user_id, total_price, and status +* Manages order items with order_item_id, order_id, product_id, quantity, and price_at_order +* Uses Jakarta EE 10.0 and MicroProfile 7.1 +* Runs on Open Liberty runtime + +== Running the Application + +There are multiple ways to run the application: + +=== Using Maven + +[source,bash] +---- +cd order +mvn liberty:run +---- + +=== Using the provided script + +[source,bash] +---- +./run.sh +---- + +=== Using Docker + +[source,bash] +---- +./run-docker.sh +---- + +This will start the Open Liberty server on port 8050 (HTTP) and 8051 (HTTPS). + +== API Endpoints + +[cols="2,4,4", options="header"] +|=== +| Method | URL | Description + +| GET +| /api/orders +| Get all orders + +| GET +| /api/orders/{id} +| Get order by ID + +| GET +| /api/orders/user/{userId} +| Get orders by user ID + +| GET +| /api/orders/status/{status} +| Get orders by status + +| POST +| /api/orders +| Create new order + +| PUT +| /api/orders/{id} +| Update order + +| DELETE +| /api/orders/{id} +| Delete order + +| PATCH +| /api/orders/{id}/status/{status} +| Update order status + +| GET +| /api/orderItems/order/{orderId} +| Get items for an order + +| GET +| /api/orderItems/{orderItemId} +| Get specific order item + +| POST +| /api/orderItems/order/{orderId} +| Add item to order + +| PUT +| /api/orderItems/{orderItemId} +| Update order item + +| DELETE +| /api/orderItems/{orderItemId} +| Delete order item +|=== + +== Testing with cURL + +=== Create new order + +[source,bash] +---- +curl -X POST http://localhost:8050/order/api/orders \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "totalPrice": 149.98, + "status": "CREATED", + "orderItems": [ + { + "productId": 101, + "quantity": 2, + "priceAtOrder": 49.99 + }, + { + "productId": 102, + "quantity": 1, + "priceAtOrder": 50.00 + } + ] + }' +---- + +=== Get all orders + +[source,bash] +---- +curl -X GET http://localhost:8050/order/api/orders +---- + +=== Get order by ID + +[source,bash] +---- +curl -X GET http://localhost:8050/order/api/orders/1 +---- + +=== Get orders by user ID + +[source,bash] +---- +curl -X GET http://localhost:8050/order/api/orders/user/1 +---- + +=== Update order + +[source,bash] +---- +curl -X PUT http://localhost:8050/order/api/orders/1 \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "totalPrice": 149.98, + "status": "PAID" + }' +---- + +=== Get items for an order + +[source,bash] +---- +curl -X GET http://localhost:8050/order/api/orderItems/order/1 +---- + +=== Add item to order + +[source,bash] +---- +curl -X POST http://localhost:8050/order/api/orderItems/order/1 \ + -H "Content-Type: application/json" \ + -d '{ + "productId": 103, + "quantity": 1, + "priceAtOrder": 29.99 + }' +---- + +=== Update order item + +[source,bash] +---- +curl -X PUT http://localhost:8050/order/api/orderItems/1 \ + -H "Content-Type: application/json" \ + -d '{ + "orderId": 1, + "productId": 103, + "quantity": 2, + "priceAtOrder": 29.99 + }' +---- + +=== Update order status + +[source,bash] +---- +curl -X PATCH http://localhost:8050/order/api/orders/1/status/SHIPPED +---- + +=== Delete order item + +[source,bash] +---- +curl -X DELETE http://localhost:8050/order/api/orderItems/1 +---- + +=== Delete order + +[source,bash] +---- +curl -X DELETE http://localhost:8050/order/api/orders/1 +---- diff --git a/code/chapter11/order/README.md b/code/chapter11/order/README.md deleted file mode 100644 index e8202c7c..00000000 --- a/code/chapter11/order/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Order Service - -A Jakarta EE and MicroProfile-based REST service for order management in the Liberty Rest App demo. - -## Features - -- Provides CRUD operations for order management -- Tracks orders with order_id, user_id, total_price, and status -- Manages order items with order_item_id, order_id, product_id, quantity, and price_at_order -- Uses Jakarta EE 10.0 and MicroProfile 6.1 -- Runs on Open Liberty runtime - -## Running the Application - -There are multiple ways to run the application: - -### Using Maven - -``` -cd order -mvn liberty:run -``` - -### Using the provided script - -``` -./run.sh -``` - -### Using Docker - -``` -./run-docker.sh -``` - -This will start the Open Liberty server on port 8050 (HTTP) and 8051 (HTTPS). - -## API Endpoints - -| Method | URL | Description | -|--------|:----------------------------------------|:-------------------------------------| -| GET | /api/orders | Get all orders | -| GET | /api/orders/{id} | Get order by ID | -| GET | /api/orders/user/{userId} | Get orders by user ID | -| GET | /api/orders/status/{status} | Get orders by status | -| POST | /api/orders | Create new order | -| PUT | /api/orders/{id} | Update order | -| DELETE | /api/orders/{id} | Delete order | -| PATCH | /api/orders/{id}/status/{status} | Update order status | -| GET | /api/orders/{orderId}/items | Get items for an order | -| GET | /api/orders/items/{orderItemId} | Get specific order item | -| POST | /api/orders/{orderId}/items | Add item to order | -| PUT | /api/orders/items/{orderItemId} | Update order item | -| DELETE | /api/orders/items/{orderItemId} | Delete order item | - -## Testing with cURL - -### Create new order -``` -curl -X POST http://localhost:8050/order/api/orders \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "CREATED", - "orderItems": [ - { - "productId": 101, - "quantity": 2, - "priceAtOrder": 49.99 - }, - { - "productId": 102, - "quantity": 1, - "priceAtOrder": 50.00 - } - ] - }' -``` - -### Get all orders -``` -curl -X GET http://localhost:8050/order/api/orders -``` - -### Get order by ID -``` -curl -X GET http://localhost:8050/order/api/orders/1 -``` - -### Get orders by user ID -``` -curl -X GET http://localhost:8050/order/api/orders/user/1 -``` - -### Update order -``` -curl -X PUT http://localhost:8050/order/api/orders/1 \ - -H "Content-Type: application/json" \ - -d '{ - "userId": 1, - "totalPrice": 149.98, - "status": "PAID" - }' -``` -### Get items for an order -``` -curl -X GET http://localhost:8050/order/api/orders/1/items -``` - -### Add item to order -``` -curl -X POST http://localhost:8050/order/api/orders/1/items \ - -H "Content-Type: application/json" \ - -d '{ - "productId": 103, - "quantity": 1, - "priceAtOrder": 29.99 - }' -``` - -### Update order item -``` -curl -X PUT http://localhost:8050/order/api/orders/items/1 \ - -H "Content-Type: application/json" \ - -d '{ - "orderId": 1, - "productId": 103, - "quantity": 2, - "priceAtOrder": 29.99 - }' -``` - -### Update order status -``` -curl -X PATCH http://localhost:8050/order/api/orders/1/status/SHIPPED -``` - -### Delete order item -``` -curl -X DELETE http://localhost:8050/order/api/orders/items/1 -``` - -### Delete order -``` -curl -X DELETE http://localhost:8050/order/api/orders/1 -``` diff --git a/code/chapter11/order/pom.xml b/code/chapter11/order/pom.xml index ff7fdc97..3a0965df 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 @@ -53,7 +53,7 @@ io.openliberty.tools liberty-maven-plugin - 3.8.2 + 3.11.2 orderServer runnable 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 index aa667d2b..da8d5395 100644 --- a/code/chapter11/order/src/main/liberty/config/server.xml +++ b/code/chapter11/order/src/main/liberty/config/server.xml @@ -1,16 +1,19 @@ - + - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 + microProfile-7.1 + jakartaEE-10.0 + restfulWS + jsonp + jsonb + cdi + beanValidation + mpOpenAPI - - \ No newline at end of file + + diff --git a/code/chapter11/payment/README.adoc b/code/chapter11/payment/README.adoc index 500d8075..e484b7a3 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 @@ -14,32 +14,774 @@ This microservice is part of the Jakarta EE 10 and MicroProfile 6.1-based e-comm === GET /payment/api/payment-config * Returns all current payment configuration values -* Example: `GET http://localhost:9080/payment/api/payment-config` +* Example: `GET http://localhost:9050/payment/api/payment-config` * Response: `{"gateway.endpoint":"https://api.paymentgateway.com"}` === POST /payment/api/payment-config * Updates a payment configuration value -* Example: `POST http://localhost:9080/payment/api/payment-config` +* Example: `POST http://localhost:9050/payment/api/payment-config` * Request body: `{"key": "payment.gateway.endpoint", "value": "https://new-api.paymentgateway.com"}` * Response: `{"key":"payment.gateway.endpoint","value":"https://new-api.paymentgateway.com","message":"Configuration updated successfully"}` === POST /payment/api/authorize * Processes a payment -* Example: `POST http://localhost:9080/payment/api/authorize` +* Example: `POST http://localhost:9050/payment/api/authorize` * Response: `{"status":"success", "message":"Payment processed successfully."}` === POST /payment/api/payment-config/process-example * Example endpoint demonstrating payment processing with configuration -* Example: `POST http://localhost:9080/payment/api/payment-config/process-example` +* Example: `POST http://localhost:9050/payment/api/payment-config/process-example` * Request body: `{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}` * Response: `{"amount":100.00,"message":"Payment processed successfully","status":"success","configUsed":{"gatewayEndpoint":"https://new-api.paymentgateway.com"}}` +NOTE: This service also exposes a set of endpoints under `/payment/api/products` (`PaymentProductResource`) demonstrating additional Rest Client usage. The `/payment/api/catalog` endpoints (`ProductCatalogResource`) are documented in detail in the <> section below. + +== MicroProfile Rest Client 4.0 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/api/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/api/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/api/catalog/products + +# Get specific product (uses getProductById method) +curl http://localhost:9050/payment/api/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/api/catalog/products/1 + +# Test with different IDs +curl http://localhost:9050/payment/api/catalog/products/2 +curl http://localhost:9050/payment/api/catalog/products/3 +---- + +*Test Query Parameters:* + +[source,bash] +---- +# Test @QueryParam - Validate product price +curl "http://localhost:9050/payment/api/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/api/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/api/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/api/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/api/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/api/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/api/catalog/products/1/availability + +# 2. Validate product price before payment +curl "http://localhost:9050/payment/api/catalog/products/1/validate-price?price=999.99" + +# 3. Get product details for payment processing +curl http://localhost:9050/payment/api/catalog/products/1 +---- + +==== Test Error Scenarios + +[source,bash] +---- +# Invalid product ID (404) +curl http://localhost:9050/payment/api/catalog/products/999999 + +# Missing query parameter (400) +curl http://localhost:9050/payment/api/catalog/products/1/validate-price + +# Invalid price value (400) +curl "http://localhost:9050/payment/api/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/api/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/api/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/api/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/api/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/api/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:* + +* `src/main/java/io/microprofile/tutorial/store/payment/filter/BearerTokenFilter.java` - Authentication filter +* `src/main/java/io/microprofile/tutorial/store/payment/filter/CorrelationIdFilter.java` - Distributed tracing +* `src/main/java/io/microprofile/tutorial/store/payment/filter/RequestLoggingFilter.java` - Request logging +* `src/main/java/io/microprofile/tutorial/store/payment/filter/ResponseLoggingFilter.java` - Response logging +* `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/api/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/api/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/api/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/api/catalog/builder/products/1/env/dev + +# Get product for staging environment (will fail if service not available) +curl http://localhost:9050/payment/api/catalog/builder/products/1/env/staging + +# Get product for production environment (will fail if service not available) +curl http://localhost:9050/payment/api/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/api/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:* + +* `src/main/java/io/microprofile/tutorial/store/payment/service/ProductClientBuilderService.java` - Programmatic client creation examples +* `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` + == Building and Running the Service === Prerequisites -* JDK 17 or higher -* Maven 3.6.0 or higher +* JDK 21 or higher +* Maven 3.13.0 or higher === Local Development @@ -52,7 +794,7 @@ mvn clean package mvn liberty:run ---- -The server will start on port 9080 (HTTP) and 9081 (HTTPS). +The server will start on port 9050 (HTTP) and 9051 (HTTPS). === Docker @@ -96,21 +838,21 @@ You can test the ConfigSource endpoints using curl or any REST client: [source,bash] ---- # Get current configuration -curl -s http://localhost:9080/payment/api/payment-config | json_pp +curl -s http://localhost:9050/payment/api/payment-config | json_pp # Update configuration property curl -s -X POST -H "Content-Type: application/json" \ -d '{"key":"payment.gateway.endpoint", "value":"https://new-api.paymentgateway.com"}' \ - http://localhost:9080/payment/api/payment-config | json_pp + http://localhost:9050/payment/api/payment-config | json_pp # Test payment processing with the configuration curl -s -X POST -H "Content-Type: application/json" \ -d '{"cardNumber":"4111111111111111", "cardHolderName":"Test User", "expiryDate":"12/25", "securityCode":"123", "amount":100.00}' \ - http://localhost:9080/payment/api/payment-config/process-example | json_pp + http://localhost:9050/payment/api/payment-config/process-example | json_pp # Test basic payment authorization curl -s -X POST -H "Content-Type: application/json" \ - http://localhost:9080/payment/api/authorize | json_pp + http://localhost:9050/payment/api/authorize | json_pp ---- === Implementation Details @@ -156,7 +898,7 @@ This uses the custom ConfigSource and persists only for the current server sessi ---- curl -X POST -H "Content-Type: application/json" \ -d '{"key":"payment.gateway.endpoint", "value":"https://test-api.paymentgateway.com"}' \ - http://localhost:9080/payment/api/payment-config + http://localhost:9050/payment/api/payment-config ---- ===== 2. Using System Properties (startup) @@ -212,7 +954,7 @@ After changing a configuration property, you can verify it was updated by callin [source,bash] ---- -curl http://localhost:9080/payment/api/payment-config +curl http://localhost:9050/payment/api/payment-config ---- == Documentation @@ -221,8 +963,8 @@ curl http://localhost:9080/payment/api/payment-config The payment service automatically generates OpenAPI documentation using MicroProfile OpenAPI annotations. -* OpenAPI UI: `http://localhost:9080/payment/api/openapi-ui/` -* OpenAPI JSON: `http://localhost:9080/payment/api/openapi` +* OpenAPI UI: `http://localhost:9050/openapi/ui` +* OpenAPI JSON: `http://localhost:9050/openapi` === MicroProfile Config Specification @@ -242,12 +984,13 @@ For more information about MicroProfile Config, refer to the official documentat ==== Port Conflicts -If you encounter a port conflict when starting the server, you can change the ports in the `pom.xml` file: +If you encounter a port conflict when starting the server, change the ports in +`src/main/liberty/config/server.xml` (the ports are hardcoded there, not read from `pom.xml`): [source,xml] ---- -9080 -9081 + ---- ==== ConfigSource Not Loading diff --git a/code/chapter11/payment/pom.xml b/code/chapter11/payment/pom.xml index 12b8fada..cea1a9ad 100644 --- a/code/chapter11/payment/pom.xml +++ b/code/chapter11/payment/pom.xml @@ -12,15 +12,15 @@ UTF-8 - 17 - 17 + 21 + 21 UTF-8 UTF-8 - 9080 - 9081 + 9050 + 9051 payment @@ -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 index 8204434e..83891788 100644 --- a/code/chapter11/payment/src/main/liberty/config/server.xml +++ b/code/chapter11/payment/src/main/liberty/config/server.xml @@ -1,20 +1,22 @@ - + + microProfile-7.1 jakartaEE-10.0 - microProfile-6.1 restfulWS jsonp jsonb cdi mpConfig - mpOpenAPI + mpOpenAPI-4.1 + mpHealth mpJwt + mpRestClient-4.0 - \ 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 index 51277204..7a63b901 100755 --- a/code/chapter11/run-all-services.sh +++ b/code/chapter11/run-all-services.sh @@ -27,10 +27,10 @@ echo "Starting all services with Docker Compose..." docker-compose up -d echo "All services are running:" -echo "- User Service: https://scaling-pancake-77vj4pwq7fpjqx-6050.app.github.dev/user" -echo "- Inventory Service: https://scaling-pancake-77vj4pwq7fpjqx-7050.app.github.dev/inventory" -echo "- Order Service: https://scaling-pancake-77vj4pwq7fpjqx-8050.app.github.dev/order" -echo "- Catalog Service: https://scaling-pancake-77vj4pwq7fpjqx-5050.app.github.dev/catalog" -echo "- Payment Service: https://scaling-pancake-77vj4pwq7fpjqx-9050.app.github.dev/payment" -echo "- Shopping Cart Service: https://scaling-pancake-77vj4pwq7fpjqx-4050.app.github.dev/shoppingcart" -echo "- Shipment Service: https://scaling-pancake-77vj4pwq7fpjqx-8060.app.github.dev/shipment" +echo "- User Service: https:///user" +echo "- Inventory Service: https:///inventory" +echo "- Order Service: https:///order" +echo "- Catalog Service: https:///catalog" +echo "- Payment Service: https:///payment" +echo "- Shopping Cart Service: https:///shoppingcart" +echo "- Shipment Service: https:///shipment" \ No newline at end of file diff --git a/code/chapter11/shipment/Dockerfile b/code/chapter11/shipment/Dockerfile index 287b43d0..bd7e0d3d 100644 --- a/code/chapter11/shipment/Dockerfile +++ b/code/chapter11/shipment/Dockerfile @@ -6,22 +6,11 @@ COPY --chown=1001:0 src/main/liberty/config/ /config/ # Create the app directory COPY --chown=1001:0 target/shipment.war /config/apps/ -# Optional: Copy utility scripts -COPY --chown=1001:0 *.sh /opt/ol/helpers/ - -# Environment variables ENV VERBOSE=true - -# This is important - adds the management of vulnerability databases to allow Docker scanning -RUN dnf install -y shadow-utils - -# Set environment variable for MP config profile ENV MP_CONFIG_PROFILE=docker -EXPOSE 8060 9060 +RUN configure.sh -# Run as non-root user for security -USER 1001 +EXPOSE 8060 9060 -# Start Liberty -ENTRYPOINT ["/opt/ol/wlp/bin/server", "run", "defaultServer"] +CMD ["/opt/ol/wlp/bin/server", "run", "defaultServer"] diff --git a/code/chapter11/shipment/README.adoc b/code/chapter11/shipment/README.adoc new file mode 100644 index 00000000..b2b23de1 --- /dev/null +++ b/code/chapter11/shipment/README.adoc @@ -0,0 +1,134 @@ += Shipment Service +:toc: left +:icons: font +:source-highlighter: highlightjs + +This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. + +== Overview + +The Shipment Service is responsible for: + +* Creating shipments for orders +* Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) +* Assigning tracking numbers +* Estimating delivery dates +* Communicating with the Order Service to update order status + +== Technologies + +The Shipment Service is built using: + +* Jakarta EE 10 +* MicroProfile 7.1 +* Open Liberty +* Java 17 + +== Getting Started + +=== Prerequisites + +* JDK 17+ +* Maven 3.8+ +* Docker (for containerized deployment) + +=== Running Locally + +To build and run the service: + +[source,bash] +---- +./run.sh +---- + +This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment + +=== Running with Docker + +To build and run the service in a Docker container: + +[source,bash] +---- +./run-docker.sh +---- + +This will build a Docker image for the service and run it, exposing ports 8060 and 9060. + +== API Endpoints + +[cols="2,5,4", options="header"] +|=== +| Method | URL | Description + +| POST +| /api/shipments/orders/{orderId} +| Create a new shipment + +| GET +| /api/shipments/{shipmentId} +| Get a shipment by ID + +| GET +| /api/shipments +| Get all shipments + +| GET +| /api/shipments/status/{status} +| Get shipments by status + +| GET +| /api/shipments/orders/{orderId} +| Get shipments for an order + +| GET +| /api/shipments/tracking/{trackingNumber} +| Get a shipment by tracking number + +| PUT +| /api/shipments/{shipmentId}/status/{status} +| Update shipment status + +| PUT +| /api/shipments/{shipmentId}/carrier +| Update shipment carrier + +| PUT +| /api/shipments/{shipmentId}/tracking +| Update shipment tracking number + +| PUT +| /api/shipments/{shipmentId}/delivery-date +| Update estimated delivery date + +| PUT +| /api/shipments/{shipmentId}/notes +| Update shipment notes + +| DELETE +| /api/shipments/{shipmentId} +| Delete a shipment +|=== + +== MicroProfile Features + +The service utilizes several MicroProfile features: + +* *Config*: For external configuration +* *Health*: For liveness and readiness checks +* *Metrics*: For monitoring service performance +* *Fault Tolerance*: For resilient communication with the Order Service +* *OpenAPI*: For API documentation + +== Documentation + +API documentation is available at: + +* OpenAPI: http://localhost:8060/openapi +* Swagger UI: http://localhost:8060/openapi/ui + +== Monitoring + +Health and metrics endpoints (served at the server root, not under the `/shipment` context root): + +* Health: http://localhost:8060/health +* Metrics: http://localhost:8060/metrics diff --git a/code/chapter11/shipment/README.md b/code/chapter11/shipment/README.md deleted file mode 100644 index 41619949..00000000 --- a/code/chapter11/shipment/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Shipment Service - -This is the Shipment Service for the MicroProfile Tutorial e-commerce application. The service manages shipments for orders in the system. - -## Overview - -The Shipment Service is responsible for: -- Creating shipments for orders -- Tracking shipment status (PENDING, PROCESSING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED, RETURNED) -- Assigning tracking numbers -- Estimating delivery dates -- Communicating with the Order Service to update order status - -## Technologies - -The Shipment Service is built using: -- Jakarta EE 10 -- MicroProfile 6.1 -- Open Liberty -- Java 17 - -## Getting Started - -### Prerequisites - -- JDK 17+ -- Maven 3.8+ -- Docker (for containerized deployment) - -### Running Locally - -To build and run the service: - -```bash -./run.sh -``` - -This will build the application and start the Open Liberty server. The service will be available at: http://localhost:8060/shipment - -### Running with Docker - -To build and run the service in a Docker container: - -```bash -./run-docker.sh -``` - -This will build a Docker image for the service and run it, exposing ports 8060 and 9060. - -## API Endpoints - -| Method | URL | Description | -|--------|-------------------------------------------|--------------------------------------| -| POST | /api/shipments/orders/{orderId} | Create a new shipment | -| GET | /api/shipments/{shipmentId} | Get a shipment by ID | -| GET | /api/shipments | Get all shipments | -| GET | /api/shipments/status/{status} | Get shipments by status | -| GET | /api/shipments/orders/{orderId} | Get shipments for an order | -| GET | /api/shipments/tracking/{trackingNumber} | Get a shipment by tracking number | -| PUT | /api/shipments/{shipmentId}/status/{status} | Update shipment status | -| PUT | /api/shipments/{shipmentId}/carrier | Update shipment carrier | -| PUT | /api/shipments/{shipmentId}/tracking | Update shipment tracking number | -| PUT | /api/shipments/{shipmentId}/delivery-date | Update estimated delivery date | -| PUT | /api/shipments/{shipmentId}/notes | Update shipment notes | -| DELETE | /api/shipments/{shipmentId} | Delete a shipment | - -## MicroProfile Features - -The service utilizes several MicroProfile features: - -- **Config**: For external configuration -- **Health**: For liveness and readiness checks -- **Metrics**: For monitoring service performance -- **Fault Tolerance**: For resilient communication with the Order Service -- **OpenAPI**: For API documentation - -## Documentation - -API documentation is available at: -- OpenAPI: http://localhost:8060/shipment/openapi -- Swagger UI: http://localhost:8060/shipment/openapi/ui - -## Monitoring - -Health and metrics endpoints: -- Health: http://localhost:8060/shipment/health -- Metrics: http://localhost:8060/shipment/metrics diff --git a/code/chapter11/shipment/pom.xml b/code/chapter11/shipment/pom.xml index 9a78242c..9d05532f 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 @@ -53,7 +53,7 @@ io.openliberty.tools liberty-maven-plugin - 3.8.2 + 3.11.2 shipmentServer runnable 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/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java index 602be808..42d78a70 100644 --- a/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java +++ b/code/chapter11/shipment/src/main/java/io/microprofile/tutorial/store/shipment/resource/ShipmentResource.java @@ -27,7 +27,7 @@ /** * REST resource for shipment operations. */ -@Path("/api/shipments") +@Path("/shipments") @RequestScoped @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) diff --git a/code/chapter11/shipment/src/main/liberty/config/server.xml b/code/chapter11/shipment/src/main/liberty/config/server.xml index 1f8cd2b0..9bf874f8 100644 --- a/code/chapter11/shipment/src/main/liberty/config/server.xml +++ b/code/chapter11/shipment/src/main/liberty/config/server.xml @@ -1,18 +1,25 @@ - + - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - servlet-6.0 - pages-3.1 - microProfile-6.1 + microProfile-7.1 + jakartaEE-10.0 + restfulWS + jsonp + jsonb + cdi + beanValidation + mpConfig + mpOpenAPI + mpHealth + mpMetrics + mpFaultTolerance - - - \ No newline at end of file + + + + diff --git a/code/chapter11/shoppingcart/README.adoc b/code/chapter11/shoppingcart/README.adoc new file mode 100644 index 00000000..dcf72f3c --- /dev/null +++ b/code/chapter11/shoppingcart/README.adoc @@ -0,0 +1,95 @@ += Shopping Cart Service +:toc: left +:icons: font +:source-highlighter: highlightjs + +This microservice is part of the Jakarta EE and MicroProfile-based e-commerce application. It handles shopping cart management for users. + +== Features + +* Create and manage user shopping carts +* Add products to cart with quantity +* Update and remove cart items +* Check product availability via the Inventory Service +* Fetch product details from the Catalog Service + +== Endpoints + +=== GET /shoppingcart/api/carts +* Returns all shopping carts in the system + +=== GET /shoppingcart/api/carts/{id} +* Returns a specific shopping cart by ID + +=== GET /shoppingcart/api/carts/user/{userId} +* Returns or creates a shopping cart for a specific user + +=== POST /shoppingcart/api/carts/user/{userId} +* Creates a new shopping cart for a user + +=== POST /shoppingcart/api/carts/{cartId}/items +* Adds an item to a shopping cart +* Request body: CartItem JSON + +=== PUT /shoppingcart/api/carts/{cartId}/items/{itemId} +* Updates an item in a shopping cart +* Request body: Updated CartItem JSON + +=== DELETE /shoppingcart/api/carts/{cartId}/items/{itemId} +* Removes an item from a shopping cart + +=== DELETE /shoppingcart/api/carts/{cartId}/items +* Removes all items from a shopping cart + +=== DELETE /shoppingcart/api/carts/{cartId} +* Deletes a shopping cart + +== Cart Item JSON Example + +[source,json] +---- +{ + "productId": 1, + "quantity": 2, + "productName": "Product Name", + "price": 29.99, + "imageUrl": "product-image.jpg" +} +---- + +NOTE: `productName`, `price`, and `imageUrl` are optional; they will be fetched from the Catalog service if not provided. + +== Running the Service + +=== Local Development + +[source,bash] +---- +./run.sh +---- + +=== Docker + +[source,bash] +---- +./run-docker.sh +---- + +== Integration with Other Services + +The Shopping Cart Service integrates with: + +* *Inventory Service*: Checks product availability before adding to cart +* *Catalog Service*: Retrieves product details (name, price, image) +* *Order Service*: Indirectly, when a cart is converted to an order + +== MicroProfile Features Used + +* *Config*: For service URL configuration +* *Fault Tolerance*: Circuit breakers, timeouts, retries, and fallbacks for resilient communication +* *Health*: Liveness and readiness checks +* *OpenAPI*: API documentation + +== Swagger UI + +OpenAPI documentation is available at: `http://localhost:4050/openapi/ui` diff --git a/code/chapter11/shoppingcart/README.md b/code/chapter11/shoppingcart/README.md deleted file mode 100644 index a989bfe8..00000000 --- a/code/chapter11/shoppingcart/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Shopping Cart Service - -This microservice is part of the Jakarta EE and MicroProfile-based e-commerce application. It handles shopping cart management for users. - -## Features - -- Create and manage user shopping carts -- Add products to cart with quantity -- Update and remove cart items -- Check product availability via the Inventory Service -- Fetch product details from the Catalog Service - -## Endpoints - -### GET /shoppingcart/api/carts -- Returns all shopping carts in the system - -### GET /shoppingcart/api/carts/{id} -- Returns a specific shopping cart by ID - -### GET /shoppingcart/api/carts/user/{userId} -- Returns or creates a shopping cart for a specific user - -### POST /shoppingcart/api/carts/user/{userId} -- Creates a new shopping cart for a user - -### POST /shoppingcart/api/carts/{cartId}/items -- Adds an item to a shopping cart -- Request body: CartItem JSON - -### PUT /shoppingcart/api/carts/{cartId}/items/{itemId} -- Updates an item in a shopping cart -- Request body: Updated CartItem JSON - -### DELETE /shoppingcart/api/carts/{cartId}/items/{itemId} -- Removes an item from a shopping cart - -### DELETE /shoppingcart/api/carts/{cartId}/items -- Removes all items from a shopping cart - -### DELETE /shoppingcart/api/carts/{cartId} -- Deletes a shopping cart - -## Cart Item JSON Example - -```json -{ - "productId": 1, - "quantity": 2, - "productName": "Product Name", // Optional, will be fetched from Catalog if not provided - "price": 29.99, // Optional, will be fetched from Catalog if not provided - "imageUrl": "product-image.jpg" // Optional, will be fetched from Catalog if not provided -} -``` - -## Running the Service - -### Local Development - -```bash -./run.sh -``` - -### Docker - -```bash -./run-docker.sh -``` - -## Integration with Other Services - -The Shopping Cart Service integrates with: - -- **Inventory Service**: Checks product availability before adding to cart -- **Catalog Service**: Retrieves product details (name, price, image) -- **Order Service**: Indirectly, when a cart is converted to an order - -## MicroProfile Features Used - -- **Config**: For service URL configuration -- **Fault Tolerance**: Circuit breakers, timeouts, retries, and fallbacks for resilient communication -- **Health**: Liveness and readiness checks -- **OpenAPI**: API documentation - -## Swagger UI - -OpenAPI documentation is available at: `http://localhost:4050/shoppingcart/api/openapi-ui/` diff --git a/code/chapter11/shoppingcart/pom.xml b/code/chapter11/shoppingcart/pom.xml index 9451fea0..928e9ee1 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 @@ -53,7 +53,7 @@ io.openliberty.tools liberty-maven-plugin - 3.8.2 + 3.11.2 shoppingcartServer runnable 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/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java index e13684cc..2c028a9f 100644 --- a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/CatalogClient.java @@ -39,10 +39,10 @@ public class CatalogClient { * @param productId The product ID * @return ProductInfo containing product details */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "getProductInfoFallback") + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "getProductInfoFallback") public ProductInfo getProductInfo(Long productId) { // Check cache first if (productCache.containsKey(productId)) { diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java index b9ac4c04..166cb9fc 100644 --- a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/client/InventoryClient.java @@ -35,10 +35,10 @@ public class InventoryClient { * @param quantity The requested quantity * @return true if the product is available in the requested quantity, false otherwise */ - // @Retry(maxRetries = 3, delay = 1000, jitter = 200, unit = ChronoUnit.MILLIS) - // @Timeout(value = 5, unit = ChronoUnit.SECONDS) - // @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) - // @Fallback(fallbackMethod = "checkProductAvailabilityFallback") + @Retry(maxRetries = 3, delay = 1000, jitter = 200) + @Timeout(value = 5, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 4, failureRatio = 0.5, delay = 10000, successThreshold = 2) + @Fallback(fallbackMethod = "checkProductAvailabilityFallback") public boolean checkProductAvailability(Long productId, int quantity) { LOGGER.info(String.format("Checking availability for product %d, quantity %d", productId, quantity)); diff --git a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java index 91dc8339..9c24ee2c 100644 --- a/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java +++ b/code/chapter11/shoppingcart/src/main/java/io/microprofile/tutorial/store/shoppingcart/health/ShoppingCartHealthCheck.java @@ -1,68 +1,64 @@ -// package io.microprofile.tutorial.store.shoppingcart.health; +package io.microprofile.tutorial.store.shoppingcart.health; -// import jakarta.enterprise.context.ApplicationScoped; -// import jakarta.inject.Inject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; -// import org.eclipse.microprofile.health.HealthCheck; -// import org.eclipse.microprofile.health.HealthCheckResponse; -// import org.eclipse.microprofile.health.Liveness; -// import org.eclipse.microprofile.health.Readiness; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.eclipse.microprofile.health.Readiness; -// import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; +import io.microprofile.tutorial.store.shoppingcart.repository.ShoppingCartRepository; -// /** -// * Health checks for the Shopping Cart service. -// */ -// @ApplicationScoped -// public class ShoppingCartHealthCheck { +/** + * Health checks for the Shopping Cart service. + */ +public class ShoppingCartHealthCheck { -// @Inject -// private ShoppingCartRepository cartRepository; + /** + * Liveness check for the Shopping Cart service. + * This check ensures that the application is running. + */ + @Liveness + @ApplicationScoped + public static class LivenessCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + return HealthCheckResponse.named("shopping-cart-service-liveness") + .up() + .withData("message", "Shopping Cart Service is alive") + .build(); + } + } -// /** -// * Liveness check for the Shopping Cart service. -// * This check ensures that the application is running. -// * -// * @return A HealthCheckResponse indicating whether the service is alive -// */ -// @Liveness -// public HealthCheck shoppingCartLivenessCheck() { -// return () -> HealthCheckResponse.named("shopping-cart-service-liveness") -// .up() -// .withData("message", "Shopping Cart Service is alive") -// .build(); -// } + /** + * Readiness check for the Shopping Cart service. + * This check ensures that the application is ready to serve requests. + */ + @Readiness + @ApplicationScoped + public static class ReadinessCheck implements HealthCheck { -// /** -// * Readiness check for the Shopping Cart service. -// * This check ensures that the application is ready to serve requests. -// * In a real application, this would check dependencies like databases. -// * -// * @return A HealthCheckResponse indicating whether the service is ready -// */ -// @Readiness -// public HealthCheck shoppingCartReadinessCheck() { -// boolean isReady = true; - -// try { -// // Simple check to ensure repository is functioning -// cartRepository.findAll(); -// } catch (Exception e) { -// isReady = false; -// } - -// return () -> { -// if (isReady) { -// return HealthCheckResponse.named("shopping-cart-service-readiness") -// .up() -// .withData("message", "Shopping Cart Service is ready") -// .build(); -// } else { -// return HealthCheckResponse.named("shopping-cart-service-readiness") -// .down() -// .withData("message", "Shopping Cart Service is not ready") -// .build(); -// } -// }; -// } -// } + @Inject + private ShoppingCartRepository cartRepository; + + @Override + public HealthCheckResponse call() { + boolean isReady; + + try { + cartRepository.findAll(); + isReady = true; + } catch (Exception e) { + isReady = false; + } + + return HealthCheckResponse.named("shopping-cart-service-readiness") + .status(isReady) + .withData("message", isReady + ? "Shopping Cart Service is ready" + : "Shopping Cart Service is not ready") + .build(); + } + } +} diff --git a/code/chapter11/shoppingcart/src/main/liberty/config/server.xml b/code/chapter11/shoppingcart/src/main/liberty/config/server.xml index 6f19cdff..e3e52ea7 100644 --- a/code/chapter11/shoppingcart/src/main/liberty/config/server.xml +++ b/code/chapter11/shoppingcart/src/main/liberty/config/server.xml @@ -1,16 +1,23 @@ - + - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 + microProfile-7.1 + jakartaEE-10.0 + restfulWS + jsonp + jsonb + cdi + beanValidation + pages + mpConfig + mpOpenAPI + mpFaultTolerance + mpHealth - - - \ 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/README.adoc b/code/chapter11/user/README.adoc index fdcc577f..887ad497 100644 --- a/code/chapter11/user/README.adoc +++ b/code/chapter11/user/README.adoc @@ -13,7 +13,6 @@ The User Management Service is responsible for user operations including: * User registration and management * User profile information -* Basic authentication This service demonstrates MicroProfile and Jakarta EE technologies in a microservice architecture. @@ -26,8 +25,8 @@ The User Management Service uses the following technologies: ** Context and Dependency Injection (CDI 4.0) ** Bean Validation 3.0 ** JSON-B 3.0 -* MicroProfile 6.1 -** OpenAPI 3.1 +* MicroProfile 7.1 +** OpenAPI 4.1 * Open Liberty * Maven @@ -41,7 +40,6 @@ user/ │ │ ├── java/ │ │ │ └── io/microprofile/tutorial/store/user/ │ │ │ ├── entity/ # Domain objects -│ │ │ ├── exception/ # Custom exceptions │ │ │ ├── repository/ # Data access layer │ │ │ ├── resource/ # REST endpoints │ │ │ ├── service/ # Business logic @@ -54,7 +52,6 @@ user/ │ │ │ └── microprofile-config.properties │ │ └── webapp/ │ │ └── index.html # Welcome page -│ └── test/ # Unit and integration tests └── pom.xml # Maven configuration ---- @@ -87,8 +84,8 @@ The service exposes the following RESTful endpoints: + [source,bash] ---- -git clone https://github.com/your-org/liberty-rest-app.git -cd liberty-rest-app/user +git clone https://github.com/microprofile/microprofile-tutorial.git +cd code/chapter11/user ---- 2. Build the project: @@ -122,7 +119,7 @@ To build and run using Docker: docker build -t microprofile-tutorial/user-service . # Run the container -docker run -p 6050:6050 microprofile-tutorial/user-service +docker run -p 6050:6050 -p 6051:6051 microprofile-tutorial/user-service ---- == Configuration @@ -160,39 +157,25 @@ http://localhost:6050/openapi == Exception Handling -The service includes a comprehensive exception handling strategy: - -* Custom exceptions for domain-specific errors -* Global exception mapping to appropriate HTTP status codes -* Consistent error response format - -Error responses follow this structure: - -[source,json] ----- -{ - "errorCode": "user_not_found", - "message": "User with ID 123 not found", - "timestamp": "2023-04-15T14:30:45Z" -} ----- +`UserService` reports error conditions by throwing `jakarta.ws.rs.WebApplicationException` with a plain +message and the appropriate HTTP status; there are no custom exception classes or exception mappers in +this module, and error responses are not JSON-structured. Common error scenarios: -* 400 Bad Request - Invalid input data * 404 Not Found - Requested user doesn't exist * 409 Conflict - Email address already in use +NOTE: Bean Validation constraint violations on `User` (e.g. an empty name or invalid email on +create/update) return 400 Bad Request via the default `beanValidation` provider, not through +`UserService`. + == Testing === Running Tests -Execute unit and integration tests with: - -[source,bash] ----- -mvn test ----- +This module does not currently have a `src/test` directory; `mvn test` will run without executing +any tests. === Testing with cURL @@ -218,7 +201,7 @@ curl -X POST http://localhost:6050/user/api/users \ "email": "john@example.com", "passwordHash": "hashed_password", "address": "123 Main St", - "phone": "+1234567890" + "phoneNumber": "+1234567890" }' ---- @@ -232,7 +215,7 @@ curl -X PUT http://localhost:6050/user/api/users/1 \ "email": "john@example.com", "passwordHash": "hashed_password", "address": "456 New Address", - "phone": "+1234567890" + "phoneNumber": "+1234567890" }' ---- @@ -270,11 +253,11 @@ For production use, consider implementing a proper database persistence layer. === Logs -* Liberty server logs are in `target/liberty/wlp/usr/servers/defaultServer/logs/` +* Liberty server logs are in `target/liberty/wlp/usr/servers/userServer/logs/` * Application logs use standard JDK logging with info level by default == Further Resources * https://jakarta.ee/specifications/restful-ws/3.1/jakarta-restful-ws-spec-3.1.html[Jakarta RESTful Web Services Specification] * https://openliberty.io/docs/latest/documentation.html[Open Liberty Documentation] -* https://download.eclipse.org/microprofile/microprofile-6.1/microprofile-spec-6.1.html[MicroProfile 6.1 Specification] \ No newline at end of file +* https://download.eclipse.org/microprofile/microprofile-7.1/microprofile-spec-7.1.html[MicroProfile 7.1 Specification] \ No newline at end of file diff --git a/code/chapter11/user/pom.xml b/code/chapter11/user/pom.xml index f743ec40..eef9fbee 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 @@ -54,7 +54,7 @@ io.openliberty.tools liberty-maven-plugin - 3.8.2 + 3.11.2 userServer runnable diff --git a/code/chapter11/user/src/main/liberty/config/server.xml b/code/chapter11/user/src/main/liberty/config/server.xml index 7fbd4773..d5f334a0 100644 --- a/code/chapter11/user/src/main/liberty/config/server.xml +++ b/code/chapter11/user/src/main/liberty/config/server.xml @@ -2,15 +2,18 @@ - restfulWS-3.1 - jsonp-2.1 - jsonb-3.0 - cdi-4.0 - microProfile-6.1 + microProfile-7.1 + jakartaEE-10.0 + restfulWS + jsonp + jsonb + cdi + beanValidation + mpOpenAPI - - - \ No newline at end of file + +