-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[kotlin-server][Java][JAX-RS] Fix path shadowing (#23414) #23871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <em>other</em> tag owns a resource | ||
| * path that starts with that prefix. | ||
| */ | ||
| private final Set<String> 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<CodegenOperation> opList = operations.computeIfAbsent(co.baseName, k -> new ArrayList<>()); | ||
| opList.add(co); | ||
| } | ||
|
|
||
| allResourcePaths.add(resourcePath); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every time an operation is assigned to a tag, its full resource path is added to the global path registry. |
||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -182,7 +204,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) { | |
|
|
||
| @Override | ||
| public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) { | ||
| OperationsMap updatedObjs = jaxrsPostProcessOperations(objs); | ||
| OperationsMap updatedObjs = jaxrsPostProcessOperations(objs, allResourcePaths); | ||
| OperationMap operations = updatedObjs.getOperations(); | ||
| if (operations != null) { | ||
| List<CodegenOperation> ops = operations.getOperation(); | ||
|
|
@@ -193,99 +215,164 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo | |
| return updatedObjs; | ||
| } | ||
|
|
||
| /** Delegates to {@link #jaxrsPostProcessOperations(OperationsMap, Set)} with no shadow check. */ | ||
| static OperationsMap jaxrsPostProcessOperations(OperationsMap objs) { | ||
| return jaxrsPostProcessOperations(objs, null); | ||
| } | ||
|
|
||
| /** Post-processes operations: normalizes metadata, computes common path, applies shadowing check. */ | ||
| private static OperationsMap jaxrsPostProcessOperations(OperationsMap objs, Set<String> allResourcePaths) { | ||
| OperationMap operations = objs.getOperations(); | ||
| String commonPath = null; | ||
| if (operations != null) { | ||
| List<CodegenOperation> ops = operations.getOperation(); | ||
| for (CodegenOperation operation : ops) { | ||
| if (operation.hasConsumes == Boolean.TRUE) { | ||
| Map<String, String> 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<CodegenOperation> 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); | ||
|
Comment on lines
+235
to
+240
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before applying a common path prefix to a tag's API class, the generator checks shadowing. |
||
|
|
||
| return objs; | ||
| } | ||
|
|
||
| /** Normalizes consumes, responses, and return types for all operations. */ | ||
| private static void processOperationMetadata(List<CodegenOperation> ops) { | ||
| for (CodegenOperation operation : ops) { | ||
| if (operation.hasConsumes == Boolean.TRUE) { | ||
| Map<String, String> firstType = operation.consumes.get(0); | ||
| if (firstType != null) { | ||
| if ("multipart/form-data".equals(firstType.get("mediaType"))) { | ||
| operation.isMultipart = Boolean.TRUE; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| boolean isMultipartPost = false; | ||
| List<Map<String, String>> consumes = operation.consumes; | ||
| if (consumes != null) { | ||
| for (Map<String, String> consume : consumes) { | ||
| String mt = consume.get("mediaType"); | ||
| if (mt != null) { | ||
| if (mt.startsWith("multipart/form-data")) { | ||
| isMultipartPost = true; | ||
| } | ||
| boolean isMultipartPost = false; | ||
| List<Map<String, String>> consumes = operation.consumes; | ||
| if (consumes != null) { | ||
| for (Map<String, String> 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<CodegenResponse> responses = operation.responses; | ||
| if (responses != null) { | ||
| for (CodegenResponse resp : responses) { | ||
| if ("0".equals(resp.code)) { | ||
| resp.code = "200"; | ||
| } | ||
| List<CodegenResponse> 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<CodegenOperation> 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<CodegenOperation> 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<CodegenOperation> ops, Set<String> allResourcePaths) { | ||
| if (allResourcePaths == null || allResourcePaths.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| // Build the set of full paths owned by the current tag. | ||
| Set<String> 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<CodegenParameter> headers) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Global path registry.