This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 2e786f6d1b442092a4f0ffe90571feae5174f926 Author: Robert Lazarski <[email protected]> AuthorDate: Sun Apr 5 17:00:45 2026 -1000 openapi: implement service/operation exclusion filtering Adds two new configuration properties to OpenApiConfiguration: ignoredServices (Set<String>) Exact service names to suppress from the generated spec. Checked in shouldIncludeService() before any inclusion rule, so a listed service is never emitted even when readAllResources=true. Properties key: openapi.ignoredServices (comma-separated) ignoredOperations (Set<String>) Per-operation exclusion with two match modes: - "ServiceName/operationName" — targeted: removes only that op on that service - "operationName" — global: removes that op name across every service Evaluated in the new shouldIncludeOperation() called from generateServicePaths() before the path is added to the Paths map. Properties key: openapi.ignoredOperations (comma-separated) applyPropertiesConfiguration() now also loads openapi.ignoredRoutes, openapi.ignoredServices, and openapi.ignoredOperations from any properties source (file, system properties, known scan locations). copy() propagates both new sets. Convenience API: addIgnoredService(String), addIgnoredOperation(String). Tests (5 new in OpenApiSpecGeneratorTest): - testIgnoredServiceIsExcluded - testIgnoredQualifiedOperationIsExcluded - testIgnoredBareOperationIsExcludedAcrossAllServices - testIgnoredServicesAndOperationsProgrammaticAPI (includes copy() check) - testQualifiedIgnoredOperationDoesNotAffectOtherServices Documentation: modules/openapi/README.md — full configuration reference covering all three exclusion mechanisms (ignoredServices, ignoredOperations, ignoredRoutes), precedence order, properties file format, and Java API examples. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --- modules/openapi/README.md | 185 +++++++++++++++++++++ .../apache/axis2/openapi/OpenApiConfiguration.java | 63 +++++++ .../apache/axis2/openapi/OpenApiSpecGenerator.java | 35 ++++ .../axis2/openapi/OpenApiSpecGeneratorTest.java | 169 +++++++++++++++++++ 4 files changed, 452 insertions(+) diff --git a/modules/openapi/README.md b/modules/openapi/README.md new file mode 100644 index 0000000000..add52816ca --- /dev/null +++ b/modules/openapi/README.md @@ -0,0 +1,185 @@ +# axis2-openapi — OpenAPI Integration Module + +Auto-generates OpenAPI 3.0.1 specifications from deployed Axis2 services and serves Swagger UI at `/swagger-ui`. + +## Endpoints + +| URL | Description | +|-----|-------------| +| `/openapi.json` | OpenAPI 3.0.1 spec (JSON) | +| `/openapi.yaml` | OpenAPI 3.0.1 spec (YAML) | +| `/swagger-ui` | Interactive Swagger UI | + +## Enabling the module + +Add to `WEB-INF/conf/axis2.xml`: +```xml +<module ref="openapi"/> +``` + +Copy `axis2-openapi-<version>.jar` to `WEB-INF/modules/openapi-<version>.mar`. + +--- + +## Configuration + +Configuration is loaded in this order (later sources win): + +1. `module.xml` parameters +2. `openapi.properties` on the classpath (or the path set by `propertiesFile` module param) +3. Known locations: `META-INF/openapi.properties`, `WEB-INF/openapi.properties`, `openapi-config.properties` +4. System properties (same key names) +5. Programmatic `OpenApiConfiguration` API (highest precedence) + +### API information + +| Property key | Default | Description | +|---|---|---| +| `openapi.title` | `Apache Axis2 REST API` | Spec `info.title` | +| `openapi.description` | (auto) | Spec `info.description` | +| `openapi.version` | `1.0.0` | Spec `info.version` | +| `openapi.contact.name` | `Apache Axis2` | Contact name | +| `openapi.contact.url` | (Apache URL) | Contact URL | +| `openapi.contact.email` | — | Contact e-mail | +| `openapi.license.name` | `Apache License 2.0` | License name | +| `openapi.license.url` | (Apache URL) | License URL | +| `openapi.termsOfServiceUrl` | — | Terms of service URL | + +### Generation flags + +| Property key | Default | Description | +|---|---|---| +| `openapi.prettyPrint` | `true` | Indent JSON/YAML output | +| `openapi.readAllResources` | `true` | Include all services unless filtered | +| `openapi.swaggerUi.enabled` | `true` | Serve Swagger UI | +| `openapi.swaggerUi.version` | `4.15.5` | CDN version of Swagger UI bundle | +| `openapi.resourcePackages` | — | Comma-separated Java packages; only services whose `ServiceClass` is in these packages are included (requires `readAllResources=false`) | + +--- + +## Filtering: excluding services and operations + +Three independent mechanisms control what appears in the generated spec. All are evaluated **before** inclusion rules — an excluded entity never appears even if it would otherwise match `readAllResources` or `resourcePackages`. + +### 1. `ignoredServices` — exclude entire services by name + +Matches against `AxisService.getName()` (exact, case-sensitive). + +**Properties file:** +```properties +# Comma-separated list of service names to exclude +openapi.ignoredServices=InternalService, DebugService, AdminService +``` + +**Java API:** +```java +OpenApiConfiguration config = new OpenApiConfiguration(); +config.addIgnoredService("InternalService"); +config.addIgnoredService("DebugService"); +``` + +### 2. `ignoredOperations` — exclude specific operations + +Each entry is one of: + +| Format | Effect | +|---|---| +| `ServiceName/operationName` | Excludes that operation on that service only | +| `operationName` | Excludes that operation name on **every** service | + +**Properties file:** +```properties +# Targeted: remove one op from one service +# Global: remove an op name from all services +openapi.ignoredOperations=AdminService/nukeDatabase, internalStatus, debugPing +``` + +**Java API:** +```java +config.addIgnoredOperation("AdminService/nukeDatabase"); // targeted +config.addIgnoredOperation("internalStatus"); // global (all services) +``` + +### 3. `ignoredRoutes` — exclude by generated path pattern + +Matches against the generated path string (e.g. `/services/MyService/myOp`). +Each entry is tested as a Java regex (`String.matches()`) **or** a substring (`String.contains()`). + +```properties +openapi.ignoredRoutes=/services/internal/.*, /services/legacy/.* +``` + +**Java API:** +```java +config.addIgnoredRoute("/services/internal/.*"); +``` + +> **Prefer `ignoredServices` and `ignoredOperations`** over `ignoredRoutes` — they match on logical names rather than generated path strings and are unaffected by future path format changes. + +### Precedence within the generator + +``` +isSystemService() (hardcoded: Version, AdminService, __) + → ignoredServices + → readAllResources / resourceClasses / resourcePackages + → shouldIncludeOperation() / ignoredOperations + → isIgnoredRoute() / ignoredRoutes + → (path added to spec) +``` + +### Complete `openapi.properties` example + +Place on the classpath as `openapi.properties` (or `META-INF/openapi.properties`): + +```properties +# API identity +openapi.title=My Financial API +openapi.version=2.1.0 +openapi.description=Internal portfolio management services + +# Contact / license +openapi.contact.name=Platform Team [email protected] +openapi.license.name=Proprietary + +# Service filtering +openapi.ignoredServices=LegacySOAPService, InternalHealthCheck +openapi.ignoredOperations=AdminService/resetDatabase, debugEcho + +# UI +openapi.prettyPrint=true +openapi.swaggerUi.enabled=true +openapi.swaggerUi.version=4.15.5 +``` + +--- + +## Programmatic configuration (Java) + +```java +OpenApiConfiguration config = new OpenApiConfiguration(); + +// API info +config.setTitle("My API"); +config.setVersion("2.0.0"); + +// Exclude services +config.addIgnoredService("InternalService"); + +// Exclude operations +config.addIgnoredOperation("AdminService/nukeDatabase"); // this service only +config.addIgnoredOperation("debugPing"); // all services + +// Pass to the generator +OpenApiSpecGenerator generator = new OpenApiSpecGenerator(configContext, config); +String json = generator.generateOpenApiJson(httpRequest); +String yaml = generator.generateOpenApiYaml(httpRequest); +``` + +--- + +## Known limitations + +- **Request body schema** — all operations are typed as `object` because Axis2 JSON-RPC services use `JsonRpcMessageReceiver` and have no annotation-level parameter metadata. Schema details must be added via `OpenApiCustomizer` or by serving a static schema file. +- **GET operations** — all operations are mapped to `POST`; override via `OpenApiCustomizer` if GET endpoints are needed. +- **YAML format** — uses `jackson-dataformat-yaml` (transitive from `swagger-core`); no additional dependency required. diff --git a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java index 0bf1292163..be57f5661b 100644 --- a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java +++ b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java @@ -85,6 +85,26 @@ public class OpenApiConfiguration { /** Routes/paths to ignore during generation */ private Collection<String> ignoredRoutes = new ArrayList<>(); + /** + * Service names to exclude from the generated spec. + * Checked against {@link org.apache.axis2.description.AxisService#getName()}. + * Example: {@code ignoredServices = {"InternalService", "DebugService"}} + * Properties key: {@code openapi.ignoredServices} (comma-separated) + */ + private Set<String> ignoredServices = new HashSet<>(); + + /** + * Operations to exclude from the generated spec. + * Each entry is either: + * <ul> + * <li>{@code "ServiceName/operationName"} — excludes that operation on that service only</li> + * <li>{@code "operationName"} — excludes that operation name across all services</li> + * </ul> + * Example: {@code ignoredOperations = {"AdminService/deleteAll", "internalStatus"}} + * Properties key: {@code openapi.ignoredOperations} (comma-separated) + */ + private Set<String> ignoredOperations = new HashSet<>(); + /** Whether to scan known configuration locations */ private boolean scanKnownConfigLocations = true; @@ -243,6 +263,24 @@ public class OpenApiConfiguration { if (packages != null) { resourcePackages.addAll(Arrays.asList(packages.split("\\s*,\\s*"))); } + + // Ignored routes (comma-separated path patterns) + String routes = getProperty(props, "openapi.ignoredRoutes", null); + if (routes != null) { + ignoredRoutes.addAll(Arrays.asList(routes.split("\\s*,\\s*"))); + } + + // Ignored service names (comma-separated exact names) + String services = getProperty(props, "openapi.ignoredServices", null); + if (services != null) { + ignoredServices.addAll(Arrays.asList(services.split("\\s*,\\s*"))); + } + + // Ignored operations (comma-separated, each "ServiceName/opName" or bare "opName") + String operations = getProperty(props, "openapi.ignoredOperations", null); + if (operations != null) { + ignoredOperations.addAll(Arrays.asList(operations.split("\\s*,\\s*"))); + } } /** @@ -350,6 +388,12 @@ public class OpenApiConfiguration { public Collection<String> getIgnoredRoutes() { return ignoredRoutes; } public void setIgnoredRoutes(Collection<String> ignoredRoutes) { this.ignoredRoutes = ignoredRoutes; } + public Set<String> getIgnoredServices() { return ignoredServices; } + public void setIgnoredServices(Set<String> ignoredServices) { this.ignoredServices = ignoredServices; } + + public Set<String> getIgnoredOperations() { return ignoredOperations; } + public void setIgnoredOperations(Set<String> ignoredOperations) { this.ignoredOperations = ignoredOperations; } + public boolean isPrettyPrint() { return prettyPrint; } public void setPrettyPrint(boolean prettyPrint) { this.prettyPrint = prettyPrint; } @@ -427,6 +471,23 @@ public class OpenApiConfiguration { ignoredRoutes.add(route); } + /** + * Exclude a service from the generated spec by its exact name. + * @param serviceName value of {@link org.apache.axis2.description.AxisService#getName()} + */ + public void addIgnoredService(String serviceName) { + ignoredServices.add(serviceName); + } + + /** + * Exclude an operation from the generated spec. + * @param entry either {@code "ServiceName/operationName"} (targeted) or + * {@code "operationName"} (applies to every service) + */ + public void addIgnoredOperation(String entry) { + ignoredOperations.add(entry); + } + /** * Create a copy of this configuration. */ @@ -458,6 +519,8 @@ public class OpenApiConfiguration { copy.resourcePackages = new HashSet<>(this.resourcePackages); copy.resourceClasses = new HashSet<>(this.resourceClasses); copy.ignoredRoutes = new ArrayList<>(this.ignoredRoutes); + copy.ignoredServices = new HashSet<>(this.ignoredServices); + copy.ignoredOperations = new HashSet<>(this.ignoredOperations); copy.securityDefinitions = new HashMap<>(this.securityDefinitions); copy.swaggerUiMediaTypes = new HashMap<>(this.swaggerUiMediaTypes); diff --git a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java index 17ffd20368..a3f16faa7a 100644 --- a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java +++ b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java @@ -337,6 +337,27 @@ public class OpenApiSpecGenerator { return paths; } + /** + * Check if an operation should be included based on {@code ignoredOperations}. + * An entry matches if it equals either: + * <ul> + * <li>{@code "ServiceName/operationName"} — targeted exclusion for one service</li> + * <li>{@code "operationName"} — excludes this operation name from every service</li> + * </ul> + */ + private boolean shouldIncludeOperation(AxisService service, AxisOperation operation) { + String opName = operation.getName().getLocalPart(); + String qualified = service.getName() + "/" + opName; + + for (String entry : configuration.getIgnoredOperations()) { + if (entry.equals(qualified) || entry.equals(opName)) { + log.debug("Skipping operation excluded by ignoredOperations '" + entry + "': " + qualified); + return false; + } + } + return true; + } + /** * Generate paths for a specific service. */ @@ -347,6 +368,11 @@ public class OpenApiSpecGenerator { while (operations.hasNext()) { AxisOperation operation = operations.next(); + // Check per-operation exclusion before generating the path + if (!shouldIncludeOperation(service, operation)) { + continue; + } + // Generate path for REST operation String path = generateOperationPath(service, operation); if (path != null && !isIgnoredRoute(path)) { @@ -439,9 +465,18 @@ public class OpenApiSpecGenerator { /** * Check if a service should be included based on configuration filters. + * Exclusion is evaluated before inclusion: a service listed in + * {@code ignoredServices} is always skipped regardless of other settings. */ private boolean shouldIncludeService(AxisService service) { String serviceName = service.getName(); + + // Explicit exclusion by name takes priority over all other filters + if (configuration.getIgnoredServices().contains(serviceName)) { + log.debug("Skipping service explicitly excluded by ignoredServices: " + serviceName); + return false; + } + String servicePackage = getServicePackage(service); // If readAllResources is false, check specific resource classes/packages diff --git a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java index 0ef5f01ae4..fe08d1c01b 100644 --- a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java +++ b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java @@ -235,6 +235,175 @@ public class OpenApiSpecGeneratorTest extends TestCase { // Note: Actual path structure depends on service configuration } + // ========== Service / Operation Exclusion Tests ========== + + /** + * Test that a service listed in ignoredServices is omitted from the spec. + */ + public void testIgnoredServiceIsExcluded() throws Exception { + // Arrange + AxisService visible = new AxisService("PublicService"); + AxisOperation visibleOp = new org.apache.axis2.description.InOutAxisOperation(); + visibleOp.setName(javax.xml.namespace.QName.valueOf("getData")); + visible.addOperation(visibleOp); + + AxisService hidden = new AxisService("InternalService"); + AxisOperation hiddenOp = new org.apache.axis2.description.InOutAxisOperation(); + hiddenOp.setName(javax.xml.namespace.QName.valueOf("secretOp")); + hidden.addOperation(hiddenOp); + + axisConfiguration.addService(visible); + axisConfiguration.addService(hidden); + + OpenApiConfiguration config = new OpenApiConfiguration(); + config.addIgnoredService("InternalService"); + OpenApiSpecGenerator filtered = new OpenApiSpecGenerator(configurationContext, config); + + // Act + OpenAPI openApi = filtered.generateOpenApiSpec(mockRequest); + + // Assert + assertNotNull(openApi.getPaths()); + assertNotNull("Public service path should be present", + openApi.getPaths().get("/services/PublicService/getData")); + assertNull("Hidden service path must be absent", + openApi.getPaths().get("/services/InternalService/secretOp")); + } + + /** + * Test that a qualified "ServiceName/operationName" entry in ignoredOperations + * removes only that specific operation, leaving other operations on the same + * service in the spec. + */ + public void testIgnoredQualifiedOperationIsExcluded() throws Exception { + // Arrange + AxisService svc = new AxisService("DataService"); + + AxisOperation keep = new org.apache.axis2.description.InOutAxisOperation(); + keep.setName(javax.xml.namespace.QName.valueOf("publicQuery")); + svc.addOperation(keep); + + AxisOperation drop = new org.apache.axis2.description.InOutAxisOperation(); + drop.setName(javax.xml.namespace.QName.valueOf("adminDump")); + svc.addOperation(drop); + + axisConfiguration.addService(svc); + + OpenApiConfiguration config = new OpenApiConfiguration(); + config.addIgnoredOperation("DataService/adminDump"); + OpenApiSpecGenerator filtered = new OpenApiSpecGenerator(configurationContext, config); + + // Act + OpenAPI openApi = filtered.generateOpenApiSpec(mockRequest); + + // Assert + assertNotNull("publicQuery must remain", openApi.getPaths().get("/services/DataService/publicQuery")); + assertNull("adminDump must be excluded", openApi.getPaths().get("/services/DataService/adminDump")); + } + + /** + * Test that a bare operation name (no service prefix) in ignoredOperations + * suppresses that operation across every service. + */ + public void testIgnoredBareOperationIsExcludedAcrossAllServices() throws Exception { + // Arrange — two services each with the banned operation name + AxisService svc1 = new AxisService("ServiceAlpha"); + AxisOperation alpha_good = new org.apache.axis2.description.InOutAxisOperation(); + alpha_good.setName(javax.xml.namespace.QName.valueOf("doWork")); + svc1.addOperation(alpha_good); + AxisOperation alpha_bad = new org.apache.axis2.description.InOutAxisOperation(); + alpha_bad.setName(javax.xml.namespace.QName.valueOf("internalStatus")); + svc1.addOperation(alpha_bad); + + AxisService svc2 = new AxisService("ServiceBeta"); + AxisOperation beta_good = new org.apache.axis2.description.InOutAxisOperation(); + beta_good.setName(javax.xml.namespace.QName.valueOf("doWork")); + svc2.addOperation(beta_good); + AxisOperation beta_bad = new org.apache.axis2.description.InOutAxisOperation(); + beta_bad.setName(javax.xml.namespace.QName.valueOf("internalStatus")); + svc2.addOperation(beta_bad); + + axisConfiguration.addService(svc1); + axisConfiguration.addService(svc2); + + OpenApiConfiguration config = new OpenApiConfiguration(); + config.addIgnoredOperation("internalStatus"); // bare name — applies to both services + OpenApiSpecGenerator filtered = new OpenApiSpecGenerator(configurationContext, config); + + // Act + OpenAPI openApi = filtered.generateOpenApiSpec(mockRequest); + + // Assert — doWork present on both, internalStatus absent on both + assertNotNull(openApi.getPaths().get("/services/ServiceAlpha/doWork")); + assertNotNull(openApi.getPaths().get("/services/ServiceBeta/doWork")); + assertNull("internalStatus must be absent from ServiceAlpha", + openApi.getPaths().get("/services/ServiceAlpha/internalStatus")); + assertNull("internalStatus must be absent from ServiceBeta", + openApi.getPaths().get("/services/ServiceBeta/internalStatus")); + } + + /** + * Test that ignoredServices and ignoredOperations are populated correctly + * via the programmatic API (the same code path exercised by properties loading + * once applyPropertiesConfiguration parses the values). + */ + public void testIgnoredServicesAndOperationsProgrammaticAPI() throws Exception { + OpenApiConfiguration config = new OpenApiConfiguration(); + config.addIgnoredService("SecretService"); + config.addIgnoredService("HiddenService"); + config.addIgnoredOperation("AdminService/nukeDatabase"); + config.addIgnoredOperation("debugPing"); + + assertTrue("SecretService should be in ignoredServices", + config.getIgnoredServices().contains("SecretService")); + assertTrue("HiddenService should be in ignoredServices", + config.getIgnoredServices().contains("HiddenService")); + assertTrue("AdminService/nukeDatabase should be in ignoredOperations", + config.getIgnoredOperations().contains("AdminService/nukeDatabase")); + assertTrue("debugPing should be in ignoredOperations", + config.getIgnoredOperations().contains("debugPing")); + + // Verify copy() preserves exclusion sets + OpenApiConfiguration copy = config.copy(); + assertTrue("copy must preserve ignoredServices", + copy.getIgnoredServices().contains("SecretService")); + assertTrue("copy must preserve ignoredOperations", + copy.getIgnoredOperations().contains("debugPing")); + } + + /** + * Test that a qualified operation entry does NOT suppress the same operation + * name on a different service (targeted exclusion, not global). + */ + public void testQualifiedIgnoredOperationDoesNotAffectOtherServices() throws Exception { + // Arrange + AxisService svcA = new AxisService("ServiceA"); + AxisOperation opA = new org.apache.axis2.description.InOutAxisOperation(); + opA.setName(javax.xml.namespace.QName.valueOf("sensitiveOp")); + svcA.addOperation(opA); + + AxisService svcB = new AxisService("ServiceB"); + AxisOperation opB = new org.apache.axis2.description.InOutAxisOperation(); + opB.setName(javax.xml.namespace.QName.valueOf("sensitiveOp")); // same name, different service + svcB.addOperation(opB); + + axisConfiguration.addService(svcA); + axisConfiguration.addService(svcB); + + OpenApiConfiguration config = new OpenApiConfiguration(); + config.addIgnoredOperation("ServiceA/sensitiveOp"); // target ServiceA only + OpenApiSpecGenerator filtered = new OpenApiSpecGenerator(configurationContext, config); + + // Act + OpenAPI openApi = filtered.generateOpenApiSpec(mockRequest); + + // Assert + assertNull("ServiceA/sensitiveOp must be excluded", + openApi.getPaths().get("/services/ServiceA/sensitiveOp")); + assertNotNull("ServiceB/sensitiveOp must remain (different service)", + openApi.getPaths().get("/services/ServiceB/sensitiveOp")); + } + /** * Test that generated JSON contains no null fields. * Jackson must be configured with Include.NON_NULL so null-valued model
