Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions code/chapter04/catalog/README-OPENAPI-4.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Chapter 04 - MicroProfile OpenAPI 4.1

This chapter demonstrates the features of MicroProfile OpenAPI 4.1, including:

## New Features Demonstrated

### 1. OpenAPI v3.1 Compatibility
The generated OpenAPI documents use the `openapi: 3.1.0` version, bringing better JSON Schema support and improved nullable handling.

### 2. Java Records Support
See `ProductRecord.java` - demonstrates how Java Records are automatically documented with proper schema generation.

Example endpoint: `GET /api/products/record/{id}`

### 3. Optional<T> Fields
See `ProductWithOptional.java` - shows how Optional fields are properly marked as nullable in the OpenAPI schema.

Example endpoint: `GET /api/products/optional/{id}`

### 4. @Target Annotation Enhancement
See `ConditionalProduct.java` - demonstrates the `@DependentRequired` annotation with proper @Target metadata.

### 5. JSON Schema Dialect Support
See `CustomModelReader.java` - shows how to specify the JSON Schema dialect using `jsonSchemaDialect` property.

To enable this reader, add to `META-INF/microprofile-config.properties`:
```
mp.openapi.model.reader=io.microprofile.tutorial.store.product.CustomModelReader
```

### 6. Extensible Interface Methods
See `ExtensionFilter.java` - demonstrates the new `getExtension()` and `hasExtension()` methods.

To enable this filter, add to `META-INF/microprofile-config.properties`:
```
mp.openapi.filter=io.microprofile.tutorial.store.product.ExtensionFilter
```

### 7. Async Operations with Callbacks
See the `processProductAsync()` method in `ProductResource.java` - demonstrates how to document asynchronous operations with callback URLs.

Example endpoint: `POST /api/products/async-process`

### 8. Security Schemes
See `SecuredProductApplication.java` - comprehensive example of documenting multiple security schemes including:
- API Key authentication
- Bearer Token (JWT)
- OAuth2 with authorization code flow
- Basic HTTP authentication

## Building and Running

This project is configured to use Java 21 as specified in the pom.xml. However, the MicroProfile OpenAPI 4.1 features demonstrated here work with Java 17+ (which includes support for Java Records).

If you have Java 21 installed:

```bash
java -version # Should show version 21
mvn clean package
mvn liberty:dev
```

If you only have Java 17, you can modify the pom.xml to use Java 17 instead (Java Records are supported since Java 16 and finalized in Java 17).

## Viewing the OpenAPI Documentation

Once the application is running, you can view the OpenAPI document at:

- YAML format: http://localhost:5050/openapi
- Swagger UI: http://localhost:5050/openapi/ui

## Key OpenAPI 4.1 Improvements

1. **Better nullable handling**: Uses JSON Schema's type arrays instead of deprecated `nullable` keyword
2. **JSON Schema 2020-12 alignment**: More robust schema validation
3. **Enhanced extension support**: New methods for working with vendor extensions
4. **Improved documentation for async APIs**: Better callback support
5. **Comprehensive security documentation**: Multiple authentication mechanisms

## References

- [MicroProfile OpenAPI 4.1 Specification](https://download.eclipse.org/microprofile/microprofile-open-api-4.1/microprofile-openapi-spec-4.1.html)
- [OpenAPI v3.1 Specification](https://spec.openapis.org/oas/v3.1.0.html)
- [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.microprofile.tutorial.store.product;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASModelReader;
import org.eclipse.microprofile.openapi.models.OpenAPI;

/**
* Custom OpenAPI model reader demonstrating jsonSchemaDialect support.
* This reader is called once during application startup to build/enhance the OpenAPI model.
*/
public class CustomModelReader implements OASModelReader {

@Override
public OpenAPI buildModel() {
// Create an OpenAPI object with jsonSchemaDialect
return OASFactory.createOpenAPI()
.openapi("3.1.0")
.jsonSchemaDialect("https://spec.openapis.org/oas/3.1/dialect/base")
.info(OASFactory.createInfo()
.title("Product API")
.version("1.0.0")
.description("API for managing products with MicroProfile OpenAPI 4.1"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.microprofile.tutorial.store.product;

import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Operation;

/**
* OpenAPI filter demonstrating the use of getExtension() and hasExtension() methods.
* This filter is called for each element in the OpenAPI model tree.
*/
public class ExtensionFilter implements OASFilter {

@Override
public Operation filterOperation(Operation operation) {
// Check if a custom extension exists using the new hasExtension method
if (operation.hasExtension("x-custom-timeout")) {
// Retrieve the extension value using the new getExtension method
Object timeout = operation.getExtension("x-custom-timeout");
System.out.println("Custom timeout found: " + timeout);

// Modify based on the extension
if (timeout != null && timeout instanceof Integer && (Integer) timeout > 30) {
operation.addExtension("x-requires-approval", true);
}
}

// Check for rate limiting extension
if (operation.hasExtension("x-rate-limit")) {
Object rateLimit = operation.getExtension("x-rate-limit");
System.out.println("Rate limit: " + rateLimit);
}

return operation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.microprofile.tutorial.store.product;

import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.eclipse.microprofile.openapi.annotations.security.SecuritySchemes;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeIn;
import org.eclipse.microprofile.openapi.annotations.security.OAuthFlows;
import org.eclipse.microprofile.openapi.annotations.security.OAuthFlow;
import org.eclipse.microprofile.openapi.annotations.security.OAuthScope;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.info.Contact;
import org.eclipse.microprofile.openapi.annotations.info.License;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

/**
* Application class demonstrating security scheme definitions in MicroProfile OpenAPI 4.1.
* This shows how to document multiple security mechanisms for your API.
*/
@ApplicationPath("/api")
@OpenAPIDefinition(
info = @Info(
title = "Secured Product API",
version = "1.0.0",
description = "Product API with multiple security schemes demonstrating MicroProfile OpenAPI 4.1 capabilities",
contact = @Contact(
name = "API Support",
email = "support@example.com",
url = "https://example.com/support"
),
license = @License(
name = "Apache 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0.html"
)
)
)
@SecuritySchemes({
@SecurityScheme(
securitySchemeName = "apiKey",
type = SecuritySchemeType.APIKEY,
description = "API Key authentication - provide your API key in the X-API-Key header",
in = SecuritySchemeIn.HEADER,
apiKeyName = "X-API-Key"
),
@SecurityScheme(
securitySchemeName = "bearerAuth",
type = SecuritySchemeType.HTTP,
description = "JWT Bearer token authentication - obtain token from /auth/login endpoint",
scheme = "bearer",
bearerFormat = "JWT"
),
@SecurityScheme(
securitySchemeName = "oauth2",
type = SecuritySchemeType.OAUTH2,
description = "OAuth2 authentication with authorization code flow",
flows = @OAuthFlows(
authorizationCode = @OAuthFlow(
authorizationUrl = "https://example.com/oauth/authorize",
tokenUrl = "https://example.com/oauth/token",
refreshUrl = "https://example.com/oauth/refresh",
scopes = {
@OAuthScope(
name = "read:products",
description = "Read product information"
),
@OAuthScope(
name = "write:products",
description = "Create and modify product information"
),
@OAuthScope(
name = "delete:products",
description = "Delete product information"
)
}
)
)
),
@SecurityScheme(
securitySchemeName = "basicAuth",
type = SecuritySchemeType.HTTP,
description = "Basic HTTP authentication",
scheme = "basic"
)
})
public class SecuredProductApplication extends Application {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.microprofile.tutorial.store.product.entity;

import org.eclipse.microprofile.openapi.annotations.media.Schema;

/**
* Request object for async product processing.
*/
@Schema(description = "Async product processing request")
public class AsyncRequest {

@Schema(description = "Product to process", required = true)
private Product product;

@Schema(description = "Callback URL to notify when processing completes",
example = "https://example.com/callback",
required = true)
private String callbackUrl;

public AsyncRequest() {
}

public AsyncRequest(Product product, String callbackUrl) {
this.product = product;
this.callbackUrl = callbackUrl;
}

public Product getProduct() {
return product;
}

public void setProduct(Product product) {
this.product = product;
}

public String getCallbackUrl() {
return callbackUrl;
}

public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.microprofile.tutorial.store.product.entity;

import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.media.DependentRequired;

/**
* Product with conditional validation.
* Demonstrates the @DependentRequired annotation in MicroProfile OpenAPI 4.1.
*/
@Schema(
description = "Product with conditional validation",
dependentRequired = {
@DependentRequired(
name = "discount",
requires = {"discountReason"}
)
}
)
public class ConditionalProduct {

@Schema(description = "Product ID", example = "1")
private Long id;

@Schema(description = "Product name", example = "Laptop", required = true)
private String name;

@Schema(description = "Product price", example = "999.99", required = true)
private Double price;

@Schema(description = "Discount amount (requires discountReason if set)", example = "100.0")
private Double discount;

@Schema(description = "Reason for discount", example = "Black Friday Sale")
private String discountReason;

public ConditionalProduct() {
}

public ConditionalProduct(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}

// Getters and setters
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Double getPrice() {
return price;
}

public void setPrice(Double price) {
this.price = price;
}

public Double getDiscount() {
return discount;
}

public void setDiscount(Double discount) {
this.discount = discount;
}

public String getDiscountReason() {
return discountReason;
}

public void setDiscountReason(String discountReason) {
this.discountReason = discountReason;
}
}
Loading