From dcc0bb3f7ba6e350bd331e978ba6a95c4d1108bc Mon Sep 17 00:00:00 2001 From: feczkob Date: Fri, 24 Apr 2026 15:03:36 +0200 Subject: [PATCH 1/3] Add tests to demonstrate the bug --- .../codegen/java/jaxrs/JavaJaxrsBaseTest.java | 54 +++++++++++++++++++ .../kotlin/KotlinServerCodegenTest.java | 36 ++++++++++++- .../src/test/resources/3_0/issue_23414.yaml | 49 +++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJaxrsBaseTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJaxrsBaseTest.java index e1e77d5a53f8..7b7b8d5d7e59 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJaxrsBaseTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJaxrsBaseTest.java @@ -261,6 +261,60 @@ public void testAddOperationToGroupUseTagsFalse() throws Exception { assertOperation(group6.get(0), "group6", "", false); } + @Test + public void testCommonPathDoesNotShadowOtherTags() throws IOException { + // Regression test for https://github.com/OpenAPITools/openapi-generator/issues/23414 + // tag-one owns /foo/bar/one and /foo/bar/two + // tag-two owns /foo/bar/three and /baz/bar/four + // The generator must NOT set commonPath="/foo/bar" for tag-one because that would + // shadow tag-two's /foo/bar/three at the JAX-RS class-level @Path routing. + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/issue_23414.yaml"); + codegen.setUseTags(true); + codegen.setOutputDir(output.getAbsolutePath()); + + DefaultGenerator generator = new DefaultGenerator(true); + generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)); + var dryRunTMan = ((DryRunTemplateManager) generator.getTemplateProcessor()).enableTemplateDataCapturing(); + generator.generate(); + + // tag-one: owns /foo/bar/one and /foo/bar/two + final var tagOneData = dryRunTMan.getCapturedTemplateData( + output.toPath().resolve("src/gen/java/org/openapitools/api/TagOneApi.java")); + String tagOneCommonPath = (String) tagOneData.get("commonPath"); + + // tag-two: owns /foo/bar/three and /baz/bar/four + final var tagTwoData = dryRunTMan.getCapturedTemplateData( + output.toPath().resolve("src/gen/java/org/openapitools/api/TagTwoApi.java")); + + // Critical assertion: TagOneApi must not claim /foo/bar as its class-level @Path + // because TagTwoApi also has /foo/bar/three — that route would be unreachable. + Assert.assertNotEquals(tagOneCommonPath, "/foo/bar", + "TagOneApi commonPath must not shadow TagTwoApi's /foo/bar/three"); + + // All four operations must still be reachable via commonPath + operation.path + List tagOneOps = getOperationsList(tagOneData); + Assert.assertEquals(tagOneOps.size(), 2); + for (CodegenOperation op : tagOneOps) { + String fullPath = tagOneCommonPath + op.path; + Assert.assertTrue( + fullPath.equals("/foo/bar/one") || fullPath.equals("/foo/bar/two"), + "Unexpected full path for tag-one operation: " + fullPath); + } + + List tagTwoOps = getOperationsList(tagTwoData); + String tagTwoCommonPath = (String) tagTwoData.get("commonPath"); + Assert.assertEquals(tagTwoOps.size(), 2); + for (CodegenOperation op : tagTwoOps) { + String fullPath = tagTwoCommonPath + op.path; + Assert.assertTrue( + fullPath.equals("/foo/bar/three") || fullPath.equals("/baz/bar/four"), + "Unexpected full path for tag-two operation: " + fullPath); + } + } + private void assertOperation(CodegenOperation op, String expectedBasename, String expectedPath, boolean expectedSubResourceOp) { Assert.assertEquals(op.path, expectedPath); Assert.assertEquals(op.baseName, expectedBasename); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index abe3fcc3da50..87176dc4eaa3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -9,13 +9,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.openapitools.codegen.CodegenOperation; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.antlr4.KotlinLexer; @@ -751,4 +751,38 @@ public void testFloatingPointMultipleOfValidationUsesTolerance() throws IOExcept "if (intVal % 2 != 0) {" ); } + + // ==================== Cross-tag path shadowing (issue #23414) ==================== + + @Test + public void testCommonPathDoesNotShadowOtherTags_jaxrsSpec() throws IOException { + // Regression test for https://github.com/OpenAPITools/openapi-generator/issues/23414 + // tag-one owns /foo/bar/one and /foo/bar/two + // tag-two owns /foo/bar/three and /baz/bar/four + // TagOneApi must NOT have @Path("/foo/bar") at class level because that would shadow + // TagTwoApi's /foo/bar/three route in the JAX-RS runtime. + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + codegen.additionalProperties().put(USE_TAGS, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/issue_23414.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path tagOneApi = Paths.get(outputPath + "/apis/TagOneApi.kt"); + Path tagTwoApi = Paths.get(outputPath + "/apis/TagTwoApi.kt"); + + // TagOneApi must NOT have @Path("/foo/bar") — this shadows TagTwoApi's /foo/bar/three + assertFileNotContains(tagOneApi, "@Path(\"/foo/bar\")"); + + // All operations must still be reachable with their full paths + assertFileContains(tagOneApi, "@Path(\"/foo/bar/one\")", "@Path(\"/foo/bar/two\")"); + assertFileContains(tagTwoApi, "@Path(\"/foo/bar/three\")", "@Path(\"/baz/bar/four\")"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml new file mode 100644 index 000000000000..014b81e96d53 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.3 +info: + title: Cross-tag path shadowing test + version: 1.0.0 +paths: + /foo/bar/one: + get: + tags: [tag-one] + operationId: getOne + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /foo/bar/two: + get: + tags: [tag-one] + operationId: getTwo + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /foo/bar/three: + get: + tags: [tag-two] + operationId: getThree + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + /baz/bar/four: + get: + tags: [tag-two] + operationId: getFour + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string From 77b64644bbf051d4e7bb840124762fb5ccbd93e4 Mon Sep 17 00:00:00 2001 From: feczkob Date: Fri, 24 Apr 2026 15:39:31 +0200 Subject: [PATCH 2/3] Fix cross-tag path shadowing in JAX-RS common path extraction --- .../AbstractJavaJAXRSServerCodegen.java | 233 ++++++++++++------ .../languages/KotlinServerCodegen.java | 91 +++++-- 2 files changed, 231 insertions(+), 93 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaJAXRSServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaJAXRSServerCodegen.java index e1c3a1759fc2..224ac1f36077 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaJAXRSServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaJAXRSServerCodegen.java @@ -21,9 +21,24 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import lombok.Setter; import org.apache.commons.lang3.StringUtils; -import org.openapitools.codegen.*; +import org.openapitools.codegen.CliOption; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenParameter; +import org.openapitools.codegen.CodegenResponse; +import org.openapitools.codegen.CodegenType; import org.openapitools.codegen.languages.features.BeanValidationFeatures; import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.OperationMap; @@ -32,10 +47,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.net.URL; -import java.util.*; - public abstract class AbstractJavaJAXRSServerCodegen extends AbstractJavaCodegen implements BeanValidationFeatures { public static final String SERVER_PORT = "serverPort"; public static final String USE_TAGS = "useTags"; @@ -70,6 +81,15 @@ public abstract class AbstractJavaJAXRSServerCodegen extends AbstractJavaCodegen protected boolean useTags = false; + /** + * All resource paths seen across every tag, collected during the first pass + * ({@link #addOperationToGroup}). Used in the second pass + * ({@link #postProcessOperationsWithModels}) to detect cross-tag path shadowing: a + * candidate {@code commonPath} is only safe if no other tag owns a resource + * path that starts with that prefix. + */ + private final Set allResourcePaths = new HashSet<>(); + private final Logger LOGGER = LoggerFactory.getLogger(AbstractJavaJAXRSServerCodegen.class); public AbstractJavaJAXRSServerCodegen() { @@ -138,6 +158,8 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera final List opList = operations.computeIfAbsent(co.baseName, k -> new ArrayList<>()); opList.add(co); } + + allResourcePaths.add(resourcePath); } @Override @@ -182,7 +204,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) { @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { - OperationsMap updatedObjs = jaxrsPostProcessOperations(objs); + OperationsMap updatedObjs = jaxrsPostProcessOperations(objs, allResourcePaths); OperationMap operations = updatedObjs.getOperations(); if (operations != null) { List ops = operations.getOperation(); @@ -193,99 +215,164 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allResourcePaths) { OperationMap operations = objs.getOperations(); - String commonPath = null; - if (operations != null) { - List ops = operations.getOperation(); - for (CodegenOperation operation : ops) { - if (operation.hasConsumes == Boolean.TRUE) { - Map firstType = operation.consumes.get(0); - if (firstType != null) { - if ("multipart/form-data".equals(firstType.get("mediaType"))) { - operation.isMultipart = Boolean.TRUE; - } + if (operations == null) { + return objs; + } + List ops = operations.getOperation(); + + processOperationMetadata(ops); + + String commonPath = computeCommonPath(ops); + + if (commonPath != null && !commonPath.isEmpty() && !"/".equals(commonPath) + && wouldShadowOtherTags(commonPath, ops, allResourcePaths)) { + commonPath = null; + } + + applyCommonPath(ops, commonPath, objs); + + return objs; + } + + /** Normalizes consumes, responses, and return types for all operations. */ + private static void processOperationMetadata(List ops) { + for (CodegenOperation operation : ops) { + if (operation.hasConsumes == Boolean.TRUE) { + Map firstType = operation.consumes.get(0); + if (firstType != null) { + if ("multipart/form-data".equals(firstType.get("mediaType"))) { + operation.isMultipart = Boolean.TRUE; } } + } - boolean isMultipartPost = false; - List> consumes = operation.consumes; - if (consumes != null) { - for (Map consume : consumes) { - String mt = consume.get("mediaType"); - if (mt != null) { - if (mt.startsWith("multipart/form-data")) { - isMultipartPost = true; - } + boolean isMultipartPost = false; + List> consumes = operation.consumes; + if (consumes != null) { + for (Map consume : consumes) { + String mt = consume.get("mediaType"); + if (mt != null) { + if (mt.startsWith("multipart/form-data")) { + isMultipartPost = true; } } } + } - for (CodegenParameter parameter : operation.allParams) { - if (isMultipartPost) { - parameter.vendorExtensions.put("x-multipart", "true"); - } + for (CodegenParameter parameter : operation.allParams) { + if (isMultipartPost) { + parameter.vendorExtensions.put("x-multipart", "true"); } + } - List responses = operation.responses; - if (responses != null) { - for (CodegenResponse resp : responses) { - if ("0".equals(resp.code)) { - resp.code = "200"; - } + List responses = operation.responses; + if (responses != null) { + for (CodegenResponse resp : responses) { + if ("0".equals(resp.code)) { + resp.code = "200"; + } - if (resp.baseType == null) { - resp.dataType = "void"; - resp.baseType = "Void"; - // set vendorExtensions.x-java-is-response-void to true as baseType is set to "Void" - resp.vendorExtensions.put("x-java-is-response-void", true); - } + if (resp.baseType == null) { + resp.dataType = "void"; + resp.baseType = "Void"; + // set vendorExtensions.x-java-is-response-void to true as baseType is set to "Void" + resp.vendorExtensions.put("x-java-is-response-void", true); + } - if ("array".equals(resp.containerType)) { - resp.containerType = "List"; - resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_SCHEMA_CONTAINER, SCHEMA_TYPE_ARRAY); - } else if ("set".equals(resp.containerType)) { - resp.containerType = "Set"; - resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_SCHEMA_CONTAINER, SCHEMA_TYPE_ARRAY); - resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_UNIQUE_ITEMS, true); - } else if ("map".equals(resp.containerType)) { - resp.containerType = "Map"; - } + if ("array".equals(resp.containerType)) { + resp.containerType = "List"; + resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_SCHEMA_CONTAINER, SCHEMA_TYPE_ARRAY); + } else if ("set".equals(resp.containerType)) { + resp.containerType = "Set"; + resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_SCHEMA_CONTAINER, SCHEMA_TYPE_ARRAY); + resp.vendorExtensions.put(X_MICROPROFILE_OPEN_API_RETURN_UNIQUE_ITEMS, true); + } else if ("map".equals(resp.containerType)) { + resp.containerType = "Map"; + } - if (resp.getResponseHeaders() != null) { - handleHeaders(resp.getResponseHeaders()); - } + if (resp.getResponseHeaders() != null) { + handleHeaders(resp.getResponseHeaders()); } } + } - if (operation.returnBaseType == null) { - operation.returnType = "void"; - operation.returnBaseType = "Void"; - // set vendorExtensions.x-java-is-response-void to true as returnBaseType is set to "Void" - operation.vendorExtensions.put("x-java-is-response-void", true); - } + if (operation.returnBaseType == null) { + operation.returnType = "void"; + operation.returnBaseType = "Void"; + // set vendorExtensions.x-java-is-response-void to true as returnBaseType is set to "Void" + operation.vendorExtensions.put("x-java-is-response-void", true); + } - if ("array".equals(operation.returnContainer)) { - operation.returnContainer = "List"; - } else if ("set".equals(operation.returnContainer)) { - operation.returnContainer = "Set"; - } else if ("map".equals(operation.returnContainer)) { - operation.returnContainer = "Map"; - } + if ("array".equals(operation.returnContainer)) { + operation.returnContainer = "List"; + } else if ("set".equals(operation.returnContainer)) { + operation.returnContainer = "Set"; + } else if ("map".equals(operation.returnContainer)) { + operation.returnContainer = "Map"; + } + } + } - if (commonPath == null) { - commonPath = operation.path; - } else { - commonPath = getCommonPath(commonPath, operation.path); - } + /** Computes the longest common path prefix shared by all operations. */ + private static String computeCommonPath(List ops) { + String commonPath = null; + for (CodegenOperation operation : ops) { + if (commonPath == null) { + commonPath = operation.path; + } else { + commonPath = getCommonPath(commonPath, operation.path); } + } + return commonPath; + } + + /** Strips {@code commonPath} from operation paths and writes it to {@code objs}; null means shadowing was detected. */ + private static void applyCommonPath(List ops, String commonPath, OperationsMap objs) { + if (commonPath == null) { + // Shadowing detected or no operations — keep full paths, set empty class-level prefix. + for (CodegenOperation co : ops) { + co.subresourceOperation = co.path.length() > 1; + } + objs.put("commonPath", StringUtils.EMPTY); + } else { for (CodegenOperation co : ops) { co.path = StringUtils.removeStart(co.path, commonPath); co.subresourceOperation = co.path.length() > 1; } objs.put("commonPath", "/".equals(commonPath) ? StringUtils.EMPTY : commonPath); } - return objs; + } + + /** Returns {@code true} if using {@code commonPath} as the class-level {@code @Path} would shadow routes of another tag. */ + private static boolean wouldShadowOtherTags(String commonPath, List ops, Set allResourcePaths) { + if (allResourcePaths == null || allResourcePaths.isEmpty()) { + return false; + } + + // Build the set of full paths owned by the current tag. + Set currentTagPaths = new HashSet<>(); + for (CodegenOperation co : ops) { + currentTagPaths.add(co.path); + } + + // Check whether any path from a different tag would be shadowed by commonPath. + for (String path : allResourcePaths) { + if (currentTagPaths.contains(path)) { + continue; // this path belongs to the current tag — not a shadow + } + if (path.startsWith(commonPath + "/") || path.equals(commonPath)) { + return true; + } + } + return false; } private static void handleHeaders(List headers) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 6a10b4587d47..7dc3e4eac4aa 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -92,6 +92,12 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa private boolean interfaceOnly = false; private boolean useBeanValidation = false; private boolean useTags = true; + /** + * All resource paths seen across every tag, collected during the first pass + * ({@link #addOperationToGroup}). Used in {@link #postProcessOperationsWithModels} to + * detect cross-tag path shadowing for the jaxrs-spec library. + */ + private final Set allResourcePaths = new HashSet<>(); private boolean useCoroutines = false; private boolean useMutiny = false; private boolean returnResponse = false; @@ -731,37 +737,33 @@ public void postProcess() { public void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map> operations) { if (useTags) { super.addOperationToGroup(tag, resourcePath, operation, co, operations); - return; + } else { + String basePath = StringUtils.substringBefore(resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath, "/"); + if (StringUtils.isEmpty(basePath) || basePath.chars().anyMatch(ch -> ch == '{' || ch == '}')) { + basePath = "default"; + } + super.addOperationToGroup(basePath, resourcePath, operation, co, operations); } - String basePath = StringUtils.substringBefore(resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath, "/"); - if (StringUtils.isEmpty(basePath) || basePath.chars().anyMatch(ch -> ch == '{' || ch == '}')) { - basePath = "default"; - } - super.addOperationToGroup(basePath, resourcePath, operation, co, operations); + allResourcePaths.add(resourcePath); } @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { OperationMap operations = objs.getOperations(); - // For JAXRS_SPEC library, compute commonPath for all library modes if (operations != null && Objects.equals(library, Constants.JAXRS_SPEC)) { List ops = operations.getOperation(); - // Compute commonPath from operations in this group (called once per API class) - String commonPath = null; - for (CodegenOperation operation : ops) { - if (commonPath == null) { - commonPath = operation.path; - } else { - commonPath = getCommonPath(commonPath, operation.path); - } - } - for (CodegenOperation co : ops) { - co.path = StringUtils.removeStart(co.path, commonPath); - co.subresourceOperation = co.path.length() > 1; + + String commonPath = computeCommonPath(ops); + + if (commonPath != null && !commonPath.isEmpty() && !"/".equals(commonPath) + && wouldShadowOtherTags(commonPath, ops, allResourcePaths)) { + commonPath = null; } - objs.put("commonPath", "/".equals(commonPath) ? StringUtils.EMPTY : commonPath); + + applyCommonPath(ops, commonPath, objs); } + // The following processing breaks the JAX-RS spec, so we only do this for the other libs. if (operations != null && !Objects.equals(library, Constants.JAXRS_SPEC)) { List ops = operations.getOperation(); @@ -855,6 +857,55 @@ private boolean isKtor2() { return Constants.KTOR2.equals(library); } + /** Computes the longest common path prefix shared by all operations. */ + private static String computeCommonPath(List ops) { + String commonPath = null; + for (CodegenOperation operation : ops) { + if (commonPath == null) { + commonPath = operation.path; + } else { + commonPath = getCommonPath(commonPath, operation.path); + } + } + return commonPath; + } + + /** Strips {@code commonPath} from operation paths and writes it to {@code objs}; null means shadowing was detected. */ + private static void applyCommonPath(List ops, String commonPath, OperationsMap objs) { + if (commonPath == null) { + for (CodegenOperation co : ops) { + co.subresourceOperation = co.path.length() > 1; + } + objs.put("commonPath", StringUtils.EMPTY); + } else { + for (CodegenOperation co : ops) { + co.path = StringUtils.removeStart(co.path, commonPath); + co.subresourceOperation = co.path.length() > 1; + } + objs.put("commonPath", "/".equals(commonPath) ? StringUtils.EMPTY : commonPath); + } + } + + /** Returns {@code true} if using {@code commonPath} as the class-level {@code @Path} would shadow routes of another tag. */ + private static boolean wouldShadowOtherTags(String commonPath, List ops, Set allResourcePaths) { + if (allResourcePaths == null || allResourcePaths.isEmpty()) { + return false; + } + Set currentTagPaths = new HashSet<>(); + for (CodegenOperation co : ops) { + currentTagPaths.add(co.path); + } + for (String path : allResourcePaths) { + if (currentTagPaths.contains(path)) { + continue; + } + if (path.startsWith(commonPath + "/") || path.equals(commonPath)) { + return true; + } + } + return false; + } + private static String getCommonPath(String path1, String path2) { final String[] parts1 = StringUtils.split(path1, "/"); final String[] parts2 = StringUtils.split(path2, "/"); From 0986dc716c176548042b1d7c9234eca8c97d1706 Mon Sep 17 00:00:00 2001 From: feczkob Date: Mon, 25 May 2026 21:11:55 +0200 Subject: [PATCH 3/3] Regenerate samples --- .../java/org/openapitools/api/FakeApi.java | 30 ++++---- .../java/org/openapitools/api/FakeApi.java | 30 ++++---- .../kotlin/org/openapitools/server/AppMain.kt | 68 +++++++++++++++++++ .../org/openapitools/server/Configuration.kt | 63 +++++++++++++++++ 4 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt diff --git a/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java b/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java index 287f3b59203f..c54871e99dc3 100644 --- a/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java +++ b/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java @@ -30,7 +30,7 @@ *

This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ * */ -@Path("/fake") +@Path("") @Api(value = "/", description = "") public interface FakeApi { @@ -41,7 +41,7 @@ public interface FakeApi { * */ @POST - @Path("/create_xml_item") + @Path("/fake/create_xml_item") @Consumes({ "application/xml", "application/xml; charset=utf-8", "application/xml; charset=utf-16", "text/xml", "text/xml; charset=utf-8", "text/xml; charset=utf-16" }) @ApiOperation(value = "creates an XmlItem", tags={ "fake" }) @ApiResponses(value = { @@ -49,7 +49,7 @@ public interface FakeApi { public void createXmlItem(@Valid @NotNull XmlItem xmlItem); @POST - @Path("/outer/boolean") + @Path("/fake/outer/boolean") @Produces({ "*/*" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -57,7 +57,7 @@ public interface FakeApi { public Boolean fakeOuterBooleanSerialize(@Valid Boolean body); @POST - @Path("/outer/composite") + @Path("/fake/outer/composite") @Produces({ "*/*" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -65,7 +65,7 @@ public interface FakeApi { public OuterComposite fakeOuterCompositeSerialize(@Valid OuterComposite body); @POST - @Path("/outer/number") + @Path("/fake/outer/number") @Produces({ "*/*" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -73,7 +73,7 @@ public interface FakeApi { public BigDecimal fakeOuterNumberSerialize(@Valid BigDecimal body); @POST - @Path("/outer/string") + @Path("/fake/outer/string") @Produces({ "*/*" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -81,7 +81,7 @@ public interface FakeApi { public String fakeOuterStringSerialize(@Valid String body); @PUT - @Path("/body-with-file-schema") + @Path("/fake/body-with-file-schema") @Consumes({ "application/json" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -89,7 +89,7 @@ public interface FakeApi { public void testBodyWithFileSchema(@Valid @NotNull FileSchemaTestClass body); @PUT - @Path("/body-with-query-params") + @Path("/fake/body-with-query-params") @Consumes({ "application/json" }) @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @@ -103,7 +103,7 @@ public interface FakeApi { * */ @PATCH - + @Path("/fake") @Consumes({ "application/json" }) @Produces({ "application/json" }) @ApiOperation(value = "To test \"client\" model", tags={ "fake" }) @@ -118,7 +118,7 @@ public interface FakeApi { * */ @POST - + @Path("/fake") @Consumes({ "application/x-www-form-urlencoded" }) @ApiOperation(value = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", tags={ "fake" }) @ApiResponses(value = { @@ -133,7 +133,7 @@ public interface FakeApi { * */ @GET - + @Path("/fake") @Consumes({ "application/x-www-form-urlencoded" }) @ApiOperation(value = "To test enum parameters", tags={ "fake" }) @io.swagger.annotations.ApiImplicitParams({ @@ -151,7 +151,7 @@ public interface FakeApi { * */ @DELETE - + @Path("/fake") @ApiOperation(value = "Fake endpoint to test group parameters (optional)", tags={ "fake" }) @ApiResponses(value = { @ApiResponse(code = 400, message = "Something wrong") }) @@ -162,7 +162,7 @@ public interface FakeApi { * */ @POST - @Path("/inline-additionalProperties") + @Path("/fake/inline-additionalProperties") @Consumes({ "application/json" }) @ApiOperation(value = "test inline additionalProperties", tags={ "fake" }) @ApiResponses(value = { @@ -174,7 +174,7 @@ public interface FakeApi { * */ @GET - @Path("/jsonFormData") + @Path("/fake/jsonFormData") @Consumes({ "application/x-www-form-urlencoded" }) @ApiOperation(value = "test json serialization of form data", tags={ "fake" }) @ApiResponses(value = { @@ -182,7 +182,7 @@ public interface FakeApi { public void testJsonFormData(@Multipart(value = "param") String param, @Multipart(value = "param2") String param2); @PUT - @Path("/test-query-parameters") + @Path("/fake/test-query-parameters") @ApiOperation(value = "", tags={ "fake" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "Success") }) diff --git a/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java b/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java index 673aee5cf776..a3d636a77f16 100644 --- a/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java +++ b/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java @@ -33,7 +33,7 @@ import javax.validation.constraints.*; import javax.validation.Valid; -@Path("/fake") +@Path("") @io.swagger.annotations.Api(description = "the Fake API") @@ -63,7 +63,7 @@ public FakeApi(@Context ServletConfig servletContext) { } @javax.ws.rs.POST - @Path("/create_xml_item") + @Path("/fake/create_xml_item") @Consumes({ "application/xml", "application/xml; charset=utf-8", "application/xml; charset=utf-16", "text/xml", "text/xml; charset=utf-8", "text/xml; charset=utf-16" }) @io.swagger.annotations.ApiOperation(value = "creates an XmlItem", notes = "this route creates an XmlItem", response = Void.class, tags={ "fake", }) @@ -75,7 +75,7 @@ public Response createXmlItem(@ApiParam(value = "XmlItem Body", required = true) return delegate.createXmlItem(xmlItem, securityContext); } @javax.ws.rs.POST - @Path("/outer/boolean") + @Path("/fake/outer/boolean") @Produces({ "*/*" }) @io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer boolean types", response = Boolean.class, tags={ "fake", }) @@ -87,7 +87,7 @@ public Response fakeOuterBooleanSerialize(@ApiParam(value = "Input boolean as po return delegate.fakeOuterBooleanSerialize(body, securityContext); } @javax.ws.rs.POST - @Path("/outer/composite") + @Path("/fake/outer/composite") @Produces({ "*/*" }) @io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of object with outer number type", response = OuterComposite.class, tags={ "fake", }) @@ -99,7 +99,7 @@ public Response fakeOuterCompositeSerialize(@ApiParam(value = "Input composite a return delegate.fakeOuterCompositeSerialize(body, securityContext); } @javax.ws.rs.POST - @Path("/outer/number") + @Path("/fake/outer/number") @Produces({ "*/*" }) @io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer number types", response = BigDecimal.class, tags={ "fake", }) @@ -111,7 +111,7 @@ public Response fakeOuterNumberSerialize(@ApiParam(value = "Input number as post return delegate.fakeOuterNumberSerialize(body, securityContext); } @javax.ws.rs.POST - @Path("/outer/string") + @Path("/fake/outer/string") @Produces({ "*/*" }) @io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer string types", response = String.class, tags={ "fake", }) @@ -123,7 +123,7 @@ public Response fakeOuterStringSerialize(@ApiParam(value = "Input string as post return delegate.fakeOuterStringSerialize(body, securityContext); } @javax.ws.rs.PUT - @Path("/body-with-file-schema") + @Path("/fake/body-with-file-schema") @Consumes({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "", notes = "For this test, the body for this request much reference a schema named `File`.", response = Void.class, tags={ "fake", }) @@ -135,7 +135,7 @@ public Response testBodyWithFileSchema(@ApiParam(value = "", required = true) @N return delegate.testBodyWithFileSchema(body, securityContext); } @javax.ws.rs.PUT - @Path("/body-with-query-params") + @Path("/fake/body-with-query-params") @Consumes({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "", notes = "", response = Void.class, tags={ "fake", }) @@ -147,7 +147,7 @@ public Response testBodyWithQueryParams(@ApiParam(value = "", required = true) @ return delegate.testBodyWithQueryParams(query, body, securityContext); } @javax.ws.rs.PATCH - + @Path("/fake") @Consumes({ "application/json" }) @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "To test \"client\" model", notes = "To test \"client\" model", response = Client.class, tags={ "fake", }) @@ -159,7 +159,7 @@ public Response testClientModel(@ApiParam(value = "client model", required = tru return delegate.testClientModel(body, securityContext); } @javax.ws.rs.POST - + @Path("/fake") @Consumes({ "application/x-www-form-urlencoded" }) @io.swagger.annotations.ApiOperation(value = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", notes = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", response = Void.class, authorizations = { @@ -175,7 +175,7 @@ public Response testEndpointParameters(@ApiParam(value = "None", required=true) return delegate.testEndpointParameters(number, _double, patternWithoutDelimiter, _byte, integer, int32, int64, _float, string, binaryBodypart, date, dateTime, password, paramCallback, securityContext); } @javax.ws.rs.GET - + @Path("/fake") @Consumes({ "application/x-www-form-urlencoded" }) @io.swagger.annotations.ApiOperation(value = "To test enum parameters", notes = "To test enum parameters", response = Void.class, tags={ "fake", }) @@ -188,7 +188,7 @@ public Response testEnumParameters(@ApiParam(value = "Header parameter enum test return delegate.testEnumParameters(enumHeaderStringArray, enumHeaderString, enumQueryStringArray, enumQueryString, enumQueryInteger, enumQueryDouble, enumFormStringArray, enumFormString, securityContext); } @javax.ws.rs.DELETE - + @Path("/fake") @io.swagger.annotations.ApiOperation(value = "Fake endpoint to test group parameters (optional)", notes = "Fake endpoint to test group parameters (optional)", response = Void.class, tags={ "fake", }) @@ -200,7 +200,7 @@ public Response testGroupParameters(@ApiParam(value = "Required String in group return delegate.testGroupParameters(requiredStringGroup, requiredBooleanGroup, requiredInt64Group, stringGroup, booleanGroup, int64Group, securityContext); } @javax.ws.rs.POST - @Path("/inline-additionalProperties") + @Path("/fake/inline-additionalProperties") @Consumes({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "test inline additionalProperties", notes = "", response = Void.class, tags={ "fake", }) @@ -212,7 +212,7 @@ public Response testInlineAdditionalProperties(@ApiParam(value = "request body", return delegate.testInlineAdditionalProperties(param, securityContext); } @javax.ws.rs.GET - @Path("/jsonFormData") + @Path("/fake/jsonFormData") @Consumes({ "application/x-www-form-urlencoded" }) @io.swagger.annotations.ApiOperation(value = "test json serialization of form data", notes = "", response = Void.class, tags={ "fake", }) @@ -224,7 +224,7 @@ public Response testJsonFormData(@ApiParam(value = "field1", required=true) @Fo return delegate.testJsonFormData(param, param2, securityContext); } @javax.ws.rs.PUT - @Path("/test-query-parameters") + @Path("/fake/test-query-parameters") @io.swagger.annotations.ApiOperation(value = "", notes = "To test the collection format in query parameters", response = Void.class, tags={ "fake", }) diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt new file mode 100644 index 000000000000..4053d223ac9c --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt @@ -0,0 +1,68 @@ +package org.openapitools.server + +import io.ktor.server.application.* +import io.ktor.http.* +import io.ktor.server.resources.* +import io.ktor.server.plugins.autohead.* +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.plugins.hsts.* +import com.codahale.metrics.Slf4jReporter +import io.ktor.server.metrics.dropwizard.* +import java.util.concurrent.TimeUnit +import io.ktor.server.routing.* +import io.ktor.serialization.kotlinx.json.json +import com.typesafe.config.ConfigFactory +import io.ktor.client.HttpClient +import io.ktor.client.engine.apache.Apache +import io.ktor.server.config.HoconApplicationConfig +import io.ktor.server.auth.* +import org.openapitools.server.infrastructure.* +import org.openapitools.server.apis.PetApi +import org.openapitools.server.apis.StoreApi +import org.openapitools.server.apis.UserApi + + +fun Application.main() { + install(DefaultHeaders) + install(DropwizardMetrics) { + val reporter = Slf4jReporter.forRegistry(registry) + .outputTo(this@main.log) + .convertRatesTo(TimeUnit.SECONDS) + .convertDurationsTo(TimeUnit.MILLISECONDS) + .build() + reporter.start(10, TimeUnit.SECONDS) + } + install(ContentNegotiation) { + json() + } + install(AutoHeadResponse) // see https://ktor.io/docs/autoheadresponse.html + install(Compression, ApplicationCompressionConfiguration()) // see https://ktor.io/docs/compression.html + install(HSTS, ApplicationHstsConfiguration()) // see https://ktor.io/docs/hsts.html + install(Resources) + install(Authentication) { + oauth("petstore_auth") { + client = HttpClient(Apache) + providerLookup = { applicationAuthProvider(this@main.environment.config) } + urlProvider = { _ -> + // TODO: define a callback url here. + "/" + } + } + // "Implement API key auth (api_key) for parameter name 'api_key'." + apiKeyAuth("api_key") { + validate { apikeyCredential: ApiKeyCredential -> + when { + apikeyCredential.value == "keyboardcat" -> ApiPrincipal(apikeyCredential) + else -> null + } + } + } + } + routing { + PetApi() + StoreApi() + UserApi() + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt new file mode 100644 index 000000000000..f78654c60f0f --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt @@ -0,0 +1,63 @@ +package org.openapitools.server + +// Use this file to hold package-level internal functions that return receiver object passed to the `install` method. +import io.ktor.http.* +import io.ktor.server.auth.* +import io.ktor.server.config.* +import io.ktor.util.* +import java.time.Duration +import java.util.concurrent.TimeUnit +import io.ktor.server.plugins.compression.* +import io.ktor.server.plugins.hsts.* + + +/** + * Application block for [HSTS] configuration. + * + * This file may be excluded in .openapi-generator-ignore, + * and application-specific configuration can be applied in this function. + * + * See http://ktor.io/features/hsts.html + */ +internal fun ApplicationHstsConfiguration(): HSTSConfig.() -> Unit { + return { + maxAgeInSeconds = TimeUnit.DAYS.toSeconds(365) + includeSubDomains = true + preload = false + + // You may also apply any custom directives supported by specific user-agent. For example: + // customDirectives.put("redirectHttpToHttps", "false") + } +} + +/** + * Application block for [Compression] configuration. + * + * This file may be excluded in .openapi-generator-ignore, + * and application-specific configuration can be applied in this function. + * + * See http://ktor.io/features/compression.html + */ +internal fun ApplicationCompressionConfiguration(): CompressionConfig.() -> Unit { + return { + gzip { + priority = 1.0 + } + deflate { + priority = 10.0 + minimumSize(1024) // condition + } + } +} + +// Defines authentication mechanisms used throughout the application. +fun applicationAuthProvider(config: ApplicationConfig): OAuthServerSettings = + OAuthServerSettings.OAuth2ServerSettings( + name = "petstore_auth", + authorizeUrl = "http://petstore.swagger.io/api/oauth/dialog", + accessTokenUrl = "", + requestMethod = HttpMethod.Get, + clientId = config.property("auth.oauth.petstore_auth.clientId").getString(), + clientSecret = config.property("auth.oauth.petstore_auth.clientSecret").getString(), + defaultScopes = listOf("write:pets", "read:pets") + )