diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index d83cdef420cf..1ff331cbe6dd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -79,6 +79,7 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -244,10 +245,18 @@ apiTemplateFiles are for API outputs only (controllers/handlers). // sort operations by default protected boolean skipSortingOperations = false; - protected final static Pattern XML_MIME_PATTERN = Pattern.compile("(?i)application\\/(.*)[+]?xml(;.*)?"); - protected final static Pattern JSON_MIME_PATTERN = Pattern.compile("(?i)application\\/json(;.*)?"); - protected final static Pattern JSON_VENDOR_MIME_PATTERN = Pattern.compile("(?i)application\\/vnd.(.*)+json(;.*)?"); + protected final static Pattern XML_MIME_PATTERN = Pattern.compile("(?i)application/(.*)[+]?xml(;.*)?"); + protected final static Pattern JSON_MIME_PATTERN = Pattern.compile("(?i)application/json(;.*)?"); + protected final static Pattern JSON_VENDOR_MIME_PATTERN = Pattern.compile("(?i)application/vnd.(.*)+json(;.*)?"); private static final Pattern COMMON_PREFIX_ENUM_NAME = Pattern.compile("[a-zA-Z0-9]+\\z"); + /** Matches a trailing run of digits at the end of a name, used by {@link #generateNextName}. */ + private static final Pattern TRAILING_DIGITS = Pattern.compile("\\d+\\z"); + /** + * Cache of removeCharRegEx string → compiled {@link Pattern} with {@link Pattern#UNICODE_CHARACTER_CLASS}. + * {@code sanitizeName} is called once per unique (name, regex, exceptions) tuple, so the regex string + * (almost always {@code "\\W"}) would otherwise be recompiled for every unique property/model name. + */ + private static final ConcurrentHashMap REMOVE_CHAR_UNICODE_PATTERN_CACHE = new ConcurrentHashMap<>(); /** * True if the code generator supports multiple class inheritance. @@ -472,7 +481,7 @@ protected ImmutableMap.Builder addMustacheLambdas() { private void registerMustacheLambdas() { ImmutableMap lambdas = addMustacheLambdas().build(); - if (lambdas.size() == 0) { + if (lambdas.isEmpty()) { return; } @@ -552,7 +561,7 @@ public Map postProcessAllModels(Map objs) List> modelsImports = modelsAttrs.getImportsOrEmpty(); for (ModelMap mo : modelsAttrs.getModels()) { CodegenModel cm = mo.getModel(); - if (cm.oneOf.size() > 0) { + if (!cm.oneOf.isEmpty()) { cm.vendorExtensions.put(X_IS_ONE_OF_INTERFACE, true); for (String one : cm.oneOf) { if (!additionalDataMap.containsKey(one)) { @@ -594,11 +603,9 @@ public Map postProcessAllModels(Map objs) * @return */ private boolean codegenPropertyIsNew(CodegenModel model, CodegenProperty property) { - return model.parentModel == null - ? false - : model.parentModel.allVars.stream().anyMatch(p -> + return model.parentModel != null && model.parentModel.allVars.stream().anyMatch(p -> p.name.equals(property.name) && - (p.dataType.equals(property.dataType) == false || p.datatypeWithEnum.equals(property.datatypeWithEnum) == false)); + (!p.dataType.equals(property.dataType) || !p.datatypeWithEnum.equals(property.datatypeWithEnum))); } /** @@ -742,7 +749,7 @@ protected void removeSelfReferenceImports(CodegenModel model) { for (CodegenProperty cp : model.allVars) { // detect self import if (cp.dataType.equalsIgnoreCase(model.classname) || - (cp.isContainer && cp.items != null && cp.items.dataType.equalsIgnoreCase(model.classname))) { + (cp.isContainer && cp.items != null && cp.items.dataType.equalsIgnoreCase(model.classname))) { model.imports.remove(model.classname); // remove self import cp.isSelfReference = true; } @@ -784,7 +791,7 @@ private List getModelDependencies(List vars) { } private void setCircularReferencesOnProperties(final String root, - final Map> dependencyMap) { + final Map> dependencyMap) { dependencyMap.getOrDefault(root, new ArrayList<>()) .forEach(prop -> { final List unvisited = @@ -797,9 +804,9 @@ private void setCircularReferencesOnProperties(final String root, } private boolean isCircularReference(final String root, - final Set visited, - final List unvisited, - final Map> dependencyMap) { + final Set visited, + final List unvisited, + final Map> dependencyMap) { for (int i = 0; i < unvisited.size(); i++) { final String next = unvisited.get(i); if (!visited.contains(next)) { @@ -832,7 +839,7 @@ public ModelsMap postProcessModelsEnum(ModelsMap objs) { CodegenModel cm = mo.getModel(); // for enum model - if (Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null) { + if (cm.isEnum && cm.allowableValues != null) { Map allowableValues = cm.allowableValues; List values = (List) allowableValues.get("values"); List> enumVars = buildEnumVars(values, cm.dataType); @@ -1123,7 +1130,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } else if (ModelUtils.isMapSchema(s)) { Schema addProps = ModelUtils.getAdditionalProperties(s); - if (addProps != null && ModelUtils.isComposedSchema(addProps)) { + if (ModelUtils.isComposedSchema(addProps)) { addOneOfNameExtension(addProps, nOneOf); addOneOfInterfaceModel(addProps, nOneOf); } @@ -1157,7 +1164,7 @@ public TemplatingEngineAdapter processTemplatingEngine(TemplatingEngineAdapter t @SuppressWarnings("static-method") public String escapeText(String input) { if (input == null) { - return input; + return null; } // remove \t, \n, \r @@ -1197,7 +1204,7 @@ public String escapeTextInSingleQuotes(String input) { @Override public String escapeTextWhileAllowingNewLines(String input) { if (input == null) { - return input; + return null; } // remove \t @@ -1209,7 +1216,7 @@ public String escapeTextWhileAllowingNewLines(String input) { StringEscapeUtils.unescapeJava( StringEscapeUtils.escapeJava(input) .replace("\\/", "/")) - .replaceAll("[\\t]", " ") + .replaceAll("\\t", " ") .replace("\\", "\\\\") .replace("\"", "\\\"")); } @@ -1231,7 +1238,7 @@ public String encodePath(String input) { @Override public String escapeUnsafeCharacters(String input) { LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " + - "unsafe characters"); + "unsafe characters"); // doing nothing by default and code generator should implement // the logic to prevent code injection // later we'll make this method abstract to make sure @@ -1248,7 +1255,7 @@ public String escapeUnsafeCharacters(String input) { @Override public String escapeQuotationMark(String input) { LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " + - "single/double quote"); + "single/double quote"); return input.replace("\"", "\\\""); } @@ -2027,35 +2034,35 @@ public void setParameterExampleValue(CodegenParameter codegenParameter) { // TODO need to revise how to obtain the example value if (codegenParameter.vendorExtensions != null && codegenParameter.vendorExtensions.containsKey("x-example")) { codegenParameter.example = Json.pretty(codegenParameter.vendorExtensions.get("x-example")); - } else if (Boolean.TRUE.equals(codegenParameter.isBoolean)) { + } else if (codegenParameter.isBoolean) { codegenParameter.example = "true"; - } else if (Boolean.TRUE.equals(codegenParameter.isLong)) { + } else if (codegenParameter.isLong) { codegenParameter.example = "789"; - } else if (Boolean.TRUE.equals(codegenParameter.isInteger)) { + } else if (codegenParameter.isInteger) { codegenParameter.example = "56"; - } else if (Boolean.TRUE.equals(codegenParameter.isFloat)) { + } else if (codegenParameter.isFloat) { codegenParameter.example = "3.4"; - } else if (Boolean.TRUE.equals(codegenParameter.isDouble)) { + } else if (codegenParameter.isDouble) { codegenParameter.example = "1.2"; - } else if (Boolean.TRUE.equals(codegenParameter.isNumber)) { + } else if (codegenParameter.isNumber) { codegenParameter.example = "8.14"; - } else if (Boolean.TRUE.equals(codegenParameter.isBinary)) { + } else if (codegenParameter.isBinary) { codegenParameter.example = "BINARY_DATA_HERE"; - } else if (Boolean.TRUE.equals(codegenParameter.isByteArray)) { + } else if (codegenParameter.isByteArray) { codegenParameter.example = "BYTE_ARRAY_DATA_HERE"; - } else if (Boolean.TRUE.equals(codegenParameter.isFile)) { + } else if (codegenParameter.isFile) { codegenParameter.example = "/path/to/file.txt"; - } else if (Boolean.TRUE.equals(codegenParameter.isDate)) { + } else if (codegenParameter.isDate) { codegenParameter.example = "2013-10-20"; - } else if (Boolean.TRUE.equals(codegenParameter.isDateTime)) { + } else if (codegenParameter.isDateTime) { codegenParameter.example = "2013-10-20T19:20:30+01:00"; - } else if (Boolean.TRUE.equals(codegenParameter.isUuid)) { + } else if (codegenParameter.isUuid) { codegenParameter.example = "38400000-8cf0-11bd-b23e-10b96e4ef00d"; - } else if (Boolean.TRUE.equals(codegenParameter.isUri)) { + } else if (codegenParameter.isUri) { codegenParameter.example = "https://openapi-generator.tech"; - } else if (Boolean.TRUE.equals(codegenParameter.isString)) { + } else if (codegenParameter.isString) { codegenParameter.example = codegenParameter.paramName + "_example"; - } else if (Boolean.TRUE.equals(codegenParameter.isFreeFormObject)) { + } else if (codegenParameter.isFreeFormObject) { codegenParameter.example = "Object"; } @@ -2664,10 +2671,10 @@ private NamedSchema(String name, Schema s, boolean required, boolean schemaIsFro this.schemaIsFromAdditionalProperties = schemaIsFromAdditionalProperties; } - private String name; - private Schema schema; - private boolean required; - private boolean schemaIsFromAdditionalProperties; + private final String name; + private final Schema schema; + private final boolean required; + private final boolean schemaIsFromAdditionalProperties; @Override public boolean equals(Object o) { @@ -2677,9 +2684,9 @@ public boolean equals(Object o) { return false; NamedSchema that = (NamedSchema) o; return Objects.equals(required, that.required) && - Objects.equals(name, that.name) && - Objects.equals(schema, that.schema) && - Objects.equals(schemaIsFromAdditionalProperties, that.schemaIsFromAdditionalProperties); + Objects.equals(name, that.name) && + Objects.equals(schema, that.schema) && + Objects.equals(schemaIsFromAdditionalProperties, that.schemaIsFromAdditionalProperties); } @Override @@ -2701,7 +2708,7 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map extractSchemaTestCases(String refToTestC } LinkedHashMap> testNameToTesCase = (LinkedHashMap>) schemaNameToTestCases.get(schemaName); for (Entry> entry : testNameToTesCase.entrySet()) { - LinkedHashMap testExample = (LinkedHashMap) entry.getValue(); + LinkedHashMap testExample = entry.getValue(); String nameInSnakeCase = toTestCaseName(entry.getKey()); Object data = processTestExampleData(testExample.get("data")); SchemaTestCase testCase = new SchemaTestCase( @@ -3096,7 +3103,7 @@ public CodegenModel fromModel(String name, Schema schema) { m.getVendorExtensions().putAll(schema.getExtensions()); } m.isAlias = (typeAliases.containsKey(name) - || isAliasOfSimpleTypes(schema)); // check if the unaliased schema is an alias of simple OAS types + || isAliasOfSimpleTypes(schema)); // check if the unaliased schema is an alias of simple OAS types m.setDiscriminator(createDiscriminator(name, schema)); if (schema.getDeprecated() != null) { @@ -3466,8 +3473,7 @@ private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList vis } } if (discriminatorsPropNames.size() > 1) { - once(LOGGER).warn("The oneOf schemas have conflicting discriminator property names. " + - "oneOf schemas must have the same property name, but found " + String.join(", ", discriminatorsPropNames)); + once(LOGGER).warn("The oneOf schemas have conflicting discriminator property names. oneOf schemas must have the same property name, but found {}", String.join(", ", discriminatorsPropNames)); } if (foundDisc != null && (hasDiscriminatorCnt + hasNullTypeCnt) == composedSchema.getOneOf().size() && discriminatorsPropNames.size() == 1) { disc.setPropertyName(foundDisc.getPropertyName()); @@ -3495,8 +3501,7 @@ private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList vis } } if (discriminatorsPropNames.size() > 1) { - once(LOGGER).warn("The anyOf schemas have conflicting discriminator property names. " + - "anyOf schemas must have the same property name, but found " + String.join(", ", discriminatorsPropNames)); + once(LOGGER).warn("The anyOf schemas have conflicting discriminator property names. anyOf schemas must have the same property name, but found {}", String.join(", ", discriminatorsPropNames)); } if (foundDisc != null && (hasDiscriminatorCnt + hasNullTypeCnt) == composedSchema.getAnyOf().size() && discriminatorsPropNames.size() == 1) { disc.setPropertyName(foundDisc.getPropertyName()); @@ -3550,7 +3555,7 @@ protected List getOneOfAnyOfDescendants(String composedSchemaName, } CodegenProperty df = discriminatorFound(composedSchemaName, sc, discPropName, new TreeSet()); String modelName = ModelUtils.getSimpleRef(ref); - if (df == null || !df.isString || df.required != true) { + if (df == null || !df.isString || !df.required) { String msgSuffix = ""; if (df == null) { msgSuffix += discPropName + " is missing from the schema, define it as required and type string"; @@ -3558,7 +3563,7 @@ protected List getOneOfAnyOfDescendants(String composedSchemaName, if (!df.isString) { msgSuffix += "invalid type for " + discPropName + ", set it to string"; } - if (df.required != true) { + if (!df.required) { String spacer = ""; if (msgSuffix.length() != 0) { spacer = ". "; @@ -3695,13 +3700,13 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch boolean matched = false; for (MappedModel uniqueDescendant : uniqueDescendants) { if (uniqueDescendant.getMappingName().equals(otherDescendant.getMappingName()) - || (uniqueDescendant.getModelName().equals(otherDescendant.getModelName()))) { + || (uniqueDescendant.getModelName().equals(otherDescendant.getModelName()))) { matched = true; break; } } - if (matched == false) { + if (!matched) { uniqueDescendants.add(otherDescendant); } } @@ -3893,9 +3898,9 @@ protected void updatePropertyForAnyType(CodegenProperty property, Schema p) { } property.isNullable = property.isNullable || - !(ModelUtils.isComposedSchema(p)) || - p.getAllOf() == null || - p.getAllOf().size() == 0; + !(ModelUtils.isComposedSchema(p)) || + p.getAllOf() == null || + p.getAllOf().size() == 0; if (languageSpecificPrimitives.contains(property.dataType)) { property.isPrimitiveType = true; } @@ -4151,7 +4156,7 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo if (referencedSchema.getNullable() != null) { property.isNullable = referencedSchema.getNullable(); } else if (referencedSchema.getExtensions() != null && - referencedSchema.getExtensions().containsKey(X_NULLABLE)) { + referencedSchema.getExtensions().containsKey(X_NULLABLE)) { property.isNullable = (Boolean) referencedSchema.getExtensions().get(X_NULLABLE); } @@ -4211,11 +4216,10 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo property.isContainer = true; if (ModelUtils.isSet(p)) { property.containerType = "set"; - property.containerTypeMapped = typeMapping.get(property.containerType); } else { property.containerType = "array"; - property.containerTypeMapped = typeMapping.get(property.containerType); } + property.containerTypeMapped = typeMapping.get(property.containerType); property.baseType = getSchemaType(p); // handle inner property @@ -4235,9 +4239,9 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo } boolean isAnyTypeWithNothingElseSet = (ModelUtils.isAnyType(p) && - (p.getProperties() == null || p.getProperties().isEmpty()) && - !ModelUtils.isComposedSchema(p) && - p.getAdditionalProperties() == null && p.getNot() == null && p.getEnum() == null); + (p.getProperties() == null || p.getProperties().isEmpty()) && + !ModelUtils.isComposedSchema(p) && + p.getAdditionalProperties() == null && p.getNot() == null && p.getEnum() == null); if (!ModelUtils.isArraySchema(p) && !ModelUtils.isMapSchema(p) && !ModelUtils.isFreeFormObject(p, openAPI) && !isAnyTypeWithNothingElseSet) { /* schemas that are not Array, not ModelUtils.isMapSchema, not isFreeFormObject, not AnyType with nothing else set @@ -4356,7 +4360,7 @@ void updateDefaultToEmptyContainer(CodegenProperty cp, Schema p) { * @param input a set of rules separated by `|` */ void parseDefaultToEmptyContainer(String input) { - String[] inputs = ((String) input).split("[|]"); + String[] inputs = input.split("[|]"); String containerType; for (String rule : inputs) { if (StringUtils.isEmpty(rule)) { @@ -4374,7 +4378,7 @@ void parseDefaultToEmptyContainer(String input) { LOGGER.error("Skipped invalid container type `{}` in `{}`.", containerType, input); } } else if (rule.startsWith("?")) { // nullable (required) - containerType = rule.substring(1, rule.length()); + containerType = rule.substring(1); if ("array".equalsIgnoreCase(containerType)) { arrayNullableDefaultToEmpty = true; } else if ("map".equalsIgnoreCase(containerType)) { @@ -4492,8 +4496,8 @@ protected Boolean isPropertyInnerMostEnum(CodegenProperty property) { protected CodegenProperty getMostInnerItems(CodegenProperty property) { CodegenProperty currentProperty = property; - while (currentProperty != null && (Boolean.TRUE.equals(currentProperty.isMap) - || Boolean.TRUE.equals(currentProperty.isArray)) && currentProperty.items != null) { + while (currentProperty != null && (currentProperty.isMap + || currentProperty.isArray) && currentProperty.items != null) { currentProperty = currentProperty.items; } return currentProperty; @@ -4512,8 +4516,8 @@ protected Map getInnerEnumAllowableValues(CodegenProperty proper */ protected void updateDataTypeWithEnumForArray(CodegenProperty property) { CodegenProperty baseItem = property.items; - while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap) - || Boolean.TRUE.equals(baseItem.isArray))) { + while (baseItem != null && (baseItem.isMap + || baseItem.isArray)) { baseItem = baseItem.items; } if (baseItem != null) { @@ -4540,8 +4544,8 @@ protected void updateDataTypeWithEnumForArray(CodegenProperty property) { */ protected void updateDataTypeWithEnumForMap(CodegenProperty property) { CodegenProperty baseItem = property.items; - while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap) - || Boolean.TRUE.equals(baseItem.isArray))) { + while (baseItem != null && (baseItem.isMap + || baseItem.isArray)) { baseItem = baseItem.items; } @@ -4602,9 +4606,9 @@ protected ApiResponse findMethodResponse(ApiResponses responses) { * @param methodResponse the default ApiResponse for the endpoint */ protected void handleMethodResponse(Operation operation, - Map schemas, - CodegenOperation op, - ApiResponse methodResponse) { + Map schemas, + CodegenOperation op, + ApiResponse methodResponse) { handleMethodResponse(operation, schemas, op, methodResponse, Collections.emptyMap()); } @@ -4618,10 +4622,10 @@ protected void handleMethodResponse(Operation operation, * @param schemaMappings mappings of external types to be omitted by unaliasing */ protected void handleMethodResponse(Operation operation, - Map schemas, - CodegenOperation op, - ApiResponse methodResponse, - Map schemaMappings) { + Map schemas, + CodegenOperation op, + ApiResponse methodResponse, + Map schemaMappings) { ApiResponse response = ModelUtils.getReferencedApiResponse(openAPI, methodResponse); Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, response)); @@ -4690,9 +4694,9 @@ protected void handleMethodResponse(Operation operation, */ @Override public CodegenOperation fromOperation(String path, - String httpMethod, - Operation operation, - List servers) { + String httpMethod, + Operation operation, + List servers) { LOGGER.debug("fromOperation => operation: {}", operation); if (operation == null) throw new RuntimeException("operation cannot be null in fromOperation"); @@ -4760,8 +4764,8 @@ public CodegenOperation fromOperation(String path, r.setContent(getContent(response.getContent(), imports, mediaTypeSchemaSuffix)); if (r.baseType != null && - !defaultIncludes.contains(r.baseType) && - !languageSpecificPrimitives.contains(r.baseType)) { + !defaultIncludes.contains(r.baseType) && + !languageSpecificPrimitives.contains(r.baseType)) { imports.add(r.baseType); } @@ -4771,26 +4775,26 @@ public CodegenOperation fromOperation(String path, } op.responses.add(r); - if (Boolean.TRUE.equals(r.isBinary) && Boolean.TRUE.equals(r.is2xx) && Boolean.FALSE.equals(op.isResponseBinary)) { + if (r.isBinary && r.is2xx && !op.isResponseBinary) { op.isResponseBinary = Boolean.TRUE; } - if (Boolean.TRUE.equals(r.isFile) && Boolean.TRUE.equals(r.is2xx) && Boolean.FALSE.equals(op.isResponseFile)) { + if (r.isFile && r.is2xx && !op.isResponseFile) { op.isResponseFile = Boolean.TRUE; } - if (Boolean.TRUE.equals(r.isDefault)) { + if (r.isDefault) { op.defaultReturnType = Boolean.TRUE; } // check if any 4xx or 5xx response has an error response object defined - if ((Boolean.TRUE.equals(r.is4xx) || Boolean.TRUE.equals(r.is5xx)) && - Boolean.FALSE.equals(r.primitiveType) && Boolean.FALSE.equals(r.simpleType)) { + if ((r.is4xx || r.is5xx) && + !r.primitiveType && !r.simpleType) { op.hasErrorResponseObject = Boolean.TRUE; } } // check if the operation can both return a 2xx response with a body and without if (op.responses.stream().anyMatch(response -> response.is2xx && response.dataType != null) && - op.responses.stream().anyMatch(response -> response.is2xx && response.dataType == null)) { + op.responses.stream().anyMatch(response -> response.is2xx && response.dataType == null)) { op.isResponseOptional = Boolean.TRUE; } @@ -4863,8 +4867,8 @@ public CodegenOperation fromOperation(String path, contentType = contentType.toLowerCase(Locale.ROOT); } if (contentType != null && - ((!(this instanceof RustAxumServerCodegen) && contentType.startsWith("application/x-www-form-urlencoded")) || - contentType.startsWith("multipart"))) { + ((!(this instanceof RustAxumServerCodegen) && contentType.startsWith("application/x-www-form-urlencoded")) || + contentType.startsWith("multipart"))) { // process form parameters formParams = fromRequestBodyToFormParameters(requestBody, imports); op.isMultipart = contentType.startsWith("multipart"); @@ -5054,23 +5058,23 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) { r.code = responseCode; switch (r.code.charAt(0)) { - case '1': - r.is1xx = true; - break; - case '2': - r.is2xx = true; - break; - case '3': - r.is3xx = true; - break; - case '4': - r.is4xx = true; - break; - case '5': - r.is5xx = true; - break; - default: - throw new RuntimeException("Invalid response code " + responseCode); + case '1': + r.is1xx = true; + break; + case '2': + r.is2xx = true; + break; + case '3': + r.is3xx = true; + break; + case '4': + r.is4xx = true; + break; + case '5': + r.is5xx = true; + break; + default: + throw new RuntimeException("Invalid response code " + responseCode); } } @@ -5341,7 +5345,7 @@ protected void updateParameterForString(CodegenParameter codegenParameter, Schem codegenParameter.isDecimal = true; codegenParameter.isPrimitiveType = true; } - if (Boolean.TRUE.equals(codegenParameter.isString)) { + if (codegenParameter.isString) { codegenParameter.isPrimitiveType = true; } } @@ -5534,7 +5538,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) } CodegenProperty codegenProperty = fromProperty(parameter.getName(), parameterSchema, false); - if (Boolean.TRUE.equals(codegenProperty.isModel)) { + if (codegenProperty.isModel) { codegenParameter.isModel = true; } @@ -5879,7 +5883,7 @@ protected String getOrGenerateOperationId(Operation operation, String path, Stri */ protected boolean needToImport(String type) { return StringUtils.isNotBlank(type) && !defaultIncludes.contains(type) - && !languageSpecificPrimitives.contains(type); + && !languageSpecificPrimitives.contains(type); } @SuppressWarnings("static-method") @@ -6014,8 +6018,7 @@ protected void addParentContainer(CodegenModel model, String name, Schema schema * @return The next name for the base name */ private static String generateNextName(String name) { - Pattern pattern = Pattern.compile("\\d+\\z"); - Matcher matcher = pattern.matcher(name); + Matcher matcher = TRAILING_DIGITS.matcher(name); if (matcher.find()) { String numStr = matcher.group(); int num = Integer.parseInt(numStr) + 1; @@ -6088,7 +6091,7 @@ protected Map unaliasPropertySchema(Map properti } protected void addVars(CodegenModel m, Map properties, List required, - Map allProperties, List allRequired) { + Map allProperties, List allRequired) { m.hasRequired = false; m.hasReadOnly = false; @@ -6169,7 +6172,7 @@ protected void addVars(IJsonSchemaValidationProperties m, List } else { final CodegenProperty cp; - if (cm != null && cm.allVars == vars && varsMap.keySet().contains(key)) { + if (cm != null && cm.allVars == vars && varsMap.containsKey(key)) { // when updating allVars, reuse the codegen property from the child model if it's already present // the goal is to avoid issues when the property is defined in both child, parent but the // definition is not identical, e.g. required vs optional, integer vs string @@ -6204,18 +6207,18 @@ protected void addVars(IJsonSchemaValidationProperties m, List } // set model's hasOnlyReadOnly to false if the property is read-only - if (!Boolean.TRUE.equals(cp.isReadOnly)) { + if (!cp.isReadOnly) { cm.hasOnlyReadOnly = false; } addImportsForPropertyType(cm, cp); // if required, add to the list "requiredVars" - if (Boolean.FALSE.equals(cp.required)) { + if (!cp.required) { cm.optionalVars.add(cp); } // if readonly, add to readOnlyVars (list of properties) - if (Boolean.TRUE.equals(cp.isReadOnly)) { + if (cp.isReadOnly) { cm.readOnlyVars.add(cp); cm.hasReadOnly = true; } else { // else add to readWriteVars (list of properties) @@ -6223,12 +6226,11 @@ protected void addVars(IJsonSchemaValidationProperties m, List cm.readWriteVars.add(cp); } - if (Boolean.FALSE.equals(cp.isNullable)) { + if (!cp.isNullable) { cm.nonNullableVars.add(cp); } } } - return; } /** @@ -6291,7 +6293,7 @@ private Boolean isAliasOfSimpleTypes(Schema schema) { // allOf with a single item if (schema.getAllOf() != null && schema.getAllOf().size() == 1 - && schema.getAllOf().get(0) instanceof Schema) { + && schema.getAllOf().get(0) instanceof Schema) { schema = unaliasSchema((Schema) schema.getAllOf().get(0)); schema = ModelUtils.getReferencedSchema(this.openAPI, schema); } @@ -6337,16 +6339,19 @@ protected String removeNonNameElementToCamelCase(final String name, final String * Not all operating systems support case-sensitive paths */ private String uniqueCaseInsensitiveString(String value, Map seenValues) { - if (seenValues.keySet().contains(value)) { + if (seenValues.containsKey(value)) { return seenValues.get(value); } - Optional> foundEntry = seenValues.entrySet().stream().filter(v -> v.getValue().toLowerCase(Locale.ROOT).equals(value.toLowerCase(Locale.ROOT))).findAny(); - if (foundEntry.isPresent()) { + // Build the set of already-used lowercase values once, to avoid O(n) re-collection per loop iteration. + Set lowercaseValues = seenValues.values().stream() + .map(v -> v.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + + if (lowercaseValues.contains(value.toLowerCase(Locale.ROOT))) { int counter = 0; String uniqueValue = value + "_" + counter; - - while (seenValues.values().stream().map(v -> v.toLowerCase(Locale.ROOT)).collect(Collectors.toList()).contains(uniqueValue.toLowerCase(Locale.ROOT))) { + while (lowercaseValues.contains(uniqueValue.toLowerCase(Locale.ROOT))) { counter++; uniqueValue = value + "_" + counter; } @@ -6758,7 +6763,10 @@ public String sanitizeName(final String name, String removeCharRegEx, ArrayList< // remove everything else other than word, number and _ // $php_variable => php_variable if (allowUnicodeIdentifiers) { //could be converted to a single line with ?: operator - modifiable = Pattern.compile(sanitizeNameOptions.getRemoveCharRegEx(), Pattern.UNICODE_CHARACTER_CLASS).matcher(modifiable).replaceAll(""); + modifiable = REMOVE_CHAR_UNICODE_PATTERN_CACHE + .computeIfAbsent(sanitizeNameOptions.getRemoveCharRegEx(), + regex -> Pattern.compile(regex, Pattern.UNICODE_CHARACTER_CLASS)) + .matcher(modifiable).replaceAll(""); } else { modifiable = modifiable.replaceAll(sanitizeNameOptions.getRemoveCharRegEx(), ""); } @@ -6767,7 +6775,7 @@ public String sanitizeName(final String name, String removeCharRegEx, ArrayList< } private String sanitizeValue(String value, String replaceMatch, String replaceValue, List exceptionList) { - if (exceptionList.size() == 0 || !exceptionList.contains(replaceMatch)) { + if (exceptionList.isEmpty() || !exceptionList.contains(replaceMatch)) { return value.replaceAll(replaceMatch, replaceValue); } return value; @@ -6810,65 +6818,65 @@ public void setParameterBooleanFlagWithCodegenProperty(CodegenParameter paramete LOGGER.error("Codegen Property cannot be null."); return; } - if (Boolean.TRUE.equals(property.isEmail) && Boolean.TRUE.equals(property.isString)) { + if (property.isEmail && property.isString) { parameter.isEmail = true; - } else if (Boolean.TRUE.equals(property.isPassword) && Boolean.TRUE.equals(property.isString)) { + } else if (property.isPassword && property.isString) { parameter.isPassword = true; - } else if (Boolean.TRUE.equals(property.isUuid) && Boolean.TRUE.equals(property.isString)) { + } else if (property.isUuid && property.isString) { parameter.isUuid = true; - } else if (Boolean.TRUE.equals(property.isByteArray)) { + } else if (property.isByteArray) { parameter.isByteArray = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isBinary)) { + } else if (property.isBinary) { parameter.isBinary = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isString)) { + } else if (property.isString) { parameter.isString = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isBoolean)) { + } else if (property.isBoolean) { parameter.isBoolean = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isLong)) { + } else if (property.isLong) { parameter.isLong = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isInteger)) { + } else if (property.isInteger) { parameter.isInteger = true; parameter.isPrimitiveType = true; - if (Boolean.TRUE.equals(property.isShort)) { + if (property.isShort) { parameter.isShort = true; - } else if (Boolean.TRUE.equals(property.isUnboundedInteger)) { + } else if (property.isUnboundedInteger) { parameter.isUnboundedInteger = true; } - } else if (Boolean.TRUE.equals(property.isDouble)) { + } else if (property.isDouble) { parameter.isDouble = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isFloat)) { + } else if (property.isFloat) { parameter.isFloat = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isDecimal)) { + } else if (property.isDecimal) { parameter.isDecimal = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isNumber)) { + } else if (property.isNumber) { parameter.isNumber = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isDate)) { + } else if (property.isDate) { parameter.isDate = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isDateTime)) { + } else if (property.isDateTime) { parameter.isDateTime = true; parameter.isPrimitiveType = true; - } else if (Boolean.TRUE.equals(property.isFreeFormObject)) { + } else if (property.isFreeFormObject) { parameter.isFreeFormObject = true; - } else if (Boolean.TRUE.equals(property.isAnyType)) { + } else if (property.isAnyType) { parameter.isAnyType = true; } else { LOGGER.debug("Property type is not primitive: {}", property.dataType); } - if (Boolean.TRUE.equals(property.isFile)) { + if (property.isFile) { parameter.isFile = true; } - if (Boolean.TRUE.equals(property.isModel)) { + if (property.isModel) { parameter.isModel = true; } } @@ -7002,7 +7010,7 @@ protected void postProcessEnumVars(List> enumVars) { long count = enumVars.stream().filter(v1 -> v1.get("name").equals(name)).count(); if (count > 1) { String uniqueEnumName = getUniqueEnumName(name, enumVars); - LOGGER.debug("Changing duplicate enumeration name from " + v.get("name") + " to " + uniqueEnumName); + LOGGER.debug("Changing duplicate enumeration name from {} to {}", v.get("name"), uniqueEnumName); v.put("name", uniqueEnumName); } }); @@ -7306,8 +7314,8 @@ public boolean hasFormParameter(Operation operation) { for (String consume : consumesInfo) { if (consume != null && - (consume.toLowerCase(Locale.ROOT).startsWith("application/x-www-form-urlencoded") || - consume.toLowerCase(Locale.ROOT).startsWith("multipart"))) { + (consume.toLowerCase(Locale.ROOT).startsWith("application/x-www-form-urlencoded") || + consume.toLowerCase(Locale.ROOT).startsWith("multipart"))) { return true; } } @@ -7426,7 +7434,7 @@ public List fromRequestBodyToFormParameters(RequestBody body, Schema original = null; // check if it's allOf (only 1 sub schema) with or without default/nullable/etc set in the top level if (ModelUtils.isAllOf(schema) && schema.getAllOf().size() == 1 && - (ModelUtils.getType(schema) == null || "object".equals(ModelUtils.getType(schema)))) { + (ModelUtils.getType(schema) == null || "object".equals(ModelUtils.getType(schema)))) { if (schema.getAllOf().get(0) instanceof Schema) { original = schema; schema = (Schema) schema.getAllOf().get(0); @@ -7547,7 +7555,7 @@ public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set codegenParameter.isDecimal = true; codegenParameter.isPrimitiveType = true; } - if (Boolean.TRUE.equals(codegenParameter.isString)) { + if (codegenParameter.isString) { codegenParameter.isPrimitiveType = true; } } else if (ModelUtils.isBooleanSchema(ps)) { @@ -7577,7 +7585,6 @@ public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set codegenParameter.setAdditionalPropertiesIsAnyType(codegenProperty.getAdditionalPropertiesIsAnyType()); codegenParameter.items = codegenProperty.items; codegenParameter.isPrimitiveType = false; - codegenParameter.items = codegenProperty.items; codegenParameter.mostInnerItems = codegenProperty.mostInnerItems; } else if (ModelUtils.isFreeFormObject(ps, openAPI)) { codegenParameter.isFreeFormObject = true; @@ -7630,7 +7637,7 @@ public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set // referenced schemas } - if (Boolean.TRUE.equals(codegenProperty.isModel)) { + if (codegenProperty.isModel) { codegenParameter.isModel = true; } @@ -7718,9 +7725,9 @@ protected void addBodyModelSchema(CodegenParameter codegenParameter, String name codegenModelDescription = codegenModel.description; } else { LOGGER.warn("The following schema has undefined (null) baseType. " + - "It could be due to form parameter defined in OpenAPI v2 spec with incorrect consumes. " + - "A correct 'consumes' for form parameters should be " + - "'application/x-www-form-urlencoded' or 'multipart/?'"); + "It could be due to form parameter defined in OpenAPI v2 spec with incorrect consumes. " + + "A correct 'consumes' for form parameters should be " + + "'application/x-www-form-urlencoded' or 'multipart/?'"); LOGGER.warn("schema: {}", schema); LOGGER.warn("codegenModel is null. Default to UNKNOWN_BASE_TYPE"); codegenModelName = "UNKNOWN_BASE_TYPE"; @@ -8002,8 +8009,8 @@ protected LinkedHashMap getContent(Content content, Se enc.getContentType(), headers, enc.getStyle().toString(), - enc.getExplode() == null ? false : enc.getExplode().booleanValue(), - enc.getAllowReserved() == null ? false : enc.getAllowReserved().booleanValue() + enc.getExplode() != null && enc.getExplode(), + enc.getAllowReserved() != null && enc.getAllowReserved() ); if (enc.getExtensions() != null) { @@ -8078,7 +8085,7 @@ public CodegenParameter fromRequestBody(RequestBody body, Set imports, S Schema original = null; // check if it's allOf (only 1 sub schema) with or without default/nullable/etc set in the top level if (ModelUtils.isAllOf(schema) && schema.getAllOf().size() == 1 && - (ModelUtils.getType(schema) == null || "object".equals(ModelUtils.getType(schema)))) { + (ModelUtils.getType(schema) == null || "object".equals(ModelUtils.getType(schema)))) { if (schema.getAllOf().get(0) instanceof Schema) { original = schema; schema = (Schema) schema.getAllOf().get(0); @@ -8230,9 +8237,8 @@ protected void addRequiredVarsMap(Schema schema, IJsonSchemaValidationProperties break; } } - if (found == false) { + if (!found) { LOGGER.warn("Property {} is not processed correctly (missing from getVars). Maybe it's a const (not yet supported) in openapi v3.1 spec.", requiredPropertyName); - continue; } } else if (schema.getAdditionalProperties() instanceof Boolean && Boolean.FALSE.equals(schema.getAdditionalProperties())) { // TODO add processing for requiredPropertyName @@ -8466,7 +8472,7 @@ protected boolean executePostProcessor(String[] commandArr) { int exitValue = p.exitValue(); if (exitValue != 0) { try (InputStreamReader inputStreamReader = new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(inputStreamReader)) { + BufferedReader br = new BufferedReader(inputStreamReader)) { StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { @@ -8577,7 +8583,7 @@ public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) { * @param name name of the parent oneOf schema */ public void addOneOfNameExtension(Schema schema, String name) { - if (schema.getOneOf() != null && schema.getOneOf().size() > 0) { + if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { schema.addExtension(X_ONE_OF_NAME, name); } } @@ -8658,8 +8664,8 @@ public boolean equals(Object o) { return false; SanitizeNameOptions that = (SanitizeNameOptions) o; return Objects.equals(getName(), that.getName()) && - Objects.equals(getRemoveCharRegEx(), that.getRemoveCharRegEx()) && - Objects.equals(getExceptions(), that.getExceptions()); + Objects.equals(getRemoveCharRegEx(), that.getRemoveCharRegEx()) && + Objects.equals(getExceptions(), that.getExceptions()); } @Override @@ -8765,7 +8771,7 @@ private List getComposedProperties(List xOfCollection, i += 1; if (dataTypeSet.contains(cp.dataType) - || (isTypeErasedGenerics() && dataTypeSet.contains(cp.baseType))) { + || (isTypeErasedGenerics() && dataTypeSet.contains(cp.baseType))) { // add "x-duplicated-data-type" to indicate if the (base) dataType already occurs before // in other sub-schemas of allOf/anyOf/oneOf cp.vendorExtensions.putIfAbsent("x-duplicated-data-type", true); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 999beb6ad3ce..0fa4b0dceea5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -86,16 +86,17 @@ public class DefaultGenerator implements Generator { private String basePath; private String basePathWithoutHost; private String contextPath; - private Map generatorPropertyDefaults = new HashMap<>(); + private final Map generatorPropertyDefaults = new HashMap<>(); /** * Retrieves an instance to the configured template processor, available after user-defined options are * applied via */ - @Getter protected TemplateProcessor templateProcessor = null; + @Getter + protected TemplateProcessor templateProcessor = null; private List userDefinedTemplates = new ArrayList<>(); - private String generatorCheck = "spring"; - private String templateCheck = "apiController.mustache"; + private final String generatorCheck = "spring"; + private final String templateCheck = "apiController.mustache"; public DefaultGenerator() { @@ -266,8 +267,7 @@ void configureGeneratorProperties() { openapiNormalizer.normalize(); } } catch (Exception e) { - LOGGER.error("An exception occurred in OpenAPI Normalizer. Please report the issue via https://github.com/openapitools/openapi-generator/issues/new/: "); - e.printStackTrace(); + LOGGER.error("An exception occurred in OpenAPI Normalizer. Please report the issue via https://github.com/openapitools/openapi-generator/issues/new/: ", e); } // resolve inline models @@ -607,10 +607,10 @@ private void generateModelsForVariable(List files, List allModel if (!processedModels.contains(key) && allSchemas.containsKey(key)) { generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key)); } else { - LOGGER.info("Type " + variable.getComplexType() + " of variable " + variable.getName() + " could not be resolve because it is not declared as a model."); + LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName()); } } else { - LOGGER.info("Type " + variable.getOpenApiType() + " of variable " + variable.getName() + " could not be resolve because it is not declared as a model."); + LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getOpenApiType(), variable.getName()); } } @@ -639,8 +639,8 @@ private Set getPropertyAsSet(String propertyName) { } return Arrays.stream(propertyRaw.split(",")) - .map(String::trim) - .collect(Collectors.toSet()); + .map(String::trim) + .collect(Collectors.toSet()); } private Set modelKeys() { @@ -665,7 +665,6 @@ private Set modelKeys() { return modelKeys; } - @SuppressWarnings("unchecked") void generateApis(List files, List allOperations, List allModels) { if (!generateApis) { // TODO: Process these anyway and present info via dryRun? @@ -1006,7 +1005,7 @@ private void generateOpenapiGeneratorIgnoreFile() { File ignoreFile = new File(ignoreFileNameTarget); // use the entries provided by the users to pre-populate .openapi-generator-ignore try { - LOGGER.info("Writing file " + ignoreFileNameTarget + " (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)"); + LOGGER.info("Writing file {} (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)", ignoreFileNameTarget); new File(config.outputFolder()).mkdirs(); if (!ignoreFile.createNewFile()) { // file may already exist, do nothing @@ -1430,7 +1429,10 @@ protected File processTemplateToFile(Map templateData, String te return processTemplateToFile(templateData, templateName, outputFilename, shouldGenerate, skippedByOption, this.config.getOutputDir()); } - private final Set seenFiles = new HashSet<>(); + /** + * Stores lowercased absolute paths for O(1) case-insensitive duplicate detection. + */ + private final Set seenFilesLower = new HashSet<>(); private File processTemplateToFile(Map templateData, String templateName, String outputFilename, boolean shouldGenerate, String skippedByOption, String intendedOutputDir) throws IOException { String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar); @@ -1443,10 +1445,10 @@ private File processTemplateToFile(Map templateData, String temp throw new RuntimeException(String.format(Locale.ROOT, "Target files must be generated within the output directory; absoluteTarget=%s outDir=%s", absoluteTarget, outDir)); } - if (seenFiles.stream().filter(f -> f.toLowerCase(Locale.ROOT).equals(absoluteTarget.toString().toLowerCase(Locale.ROOT))).findAny().isPresent()) { - LOGGER.warn("Duplicate file path detected. Not all operating systems can handle case sensitive file paths. path={}", absoluteTarget.toString()); + // O(1) case-insensitive duplicate check via a pre-lowercased shadow set + if (!seenFilesLower.add(absoluteTarget.toString().toLowerCase(Locale.ROOT))) { + LOGGER.warn("Duplicate file path detected. Not all operating systems can handle case sensitive file paths. path={}", absoluteTarget); } - seenFiles.add(absoluteTarget.toString()); return this.templateProcessor.write(templateData, templateName, target); } else { this.templateProcessor.skip(target.toPath(), String.format(Locale.ROOT, "Skipped by %s options supplied by user.", skippedByOption)); @@ -2002,10 +2004,8 @@ private void generateFilesMetadata(List files) { } }); - Collections.sort(relativePaths, (a, b) -> IOCase.SENSITIVE.checkCompareTo(a, b)); - relativePaths.forEach(relativePath -> { - sb.append(relativePath).append(System.lineSeparator()); - }); + relativePaths.sort(IOCase.SENSITIVE::checkCompareTo); + relativePaths.forEach(relativePath -> sb.append(relativePath).append(System.lineSeparator())); String targetFile = config.outputFolder() + File.separator + METADATA_DIR + File.separator + config.getFilesMetadataFilename(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/TemplateManager.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/TemplateManager.java index 5fce684170e3..b0ef14ca549c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/TemplateManager.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/TemplateManager.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Objects; import java.util.Scanner; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; /** @@ -33,6 +34,9 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor { private final Logger LOGGER = LoggerFactory.getLogger(TemplateManager.class); + /** Cache of resolved template path -> raw template content, populated on first read per run. */ + private final Map templateContentCache = new ConcurrentHashMap<>(); + /** * Constructs a new instance of a {@link TemplateManager} * @@ -75,7 +79,8 @@ private String getFullTemplateFile(String name) { */ @Override public String getFullTemplateContents(String name) { - return readTemplate(getFullTemplateFile(name)); + String fullPath = getFullTemplateFile(name); + return templateContentCache.computeIfAbsent(fullPath, this::readTemplate); } /** @@ -89,6 +94,13 @@ public Path getFullTemplatePath(String name) { return Paths.get(getFullTemplateFile(name)); } + /** + * Pre-compiled pattern for replacing the OS file separator with '/' in classpath resource paths. + * Only non-null on operating systems where {@link File#separator} is not already '/'. + */ + private static final Pattern FILE_SEP_PATTERN = + "/".equals(File.separator) ? null : Pattern.compile(Pattern.quote(File.separator)); + /** * Gets a normalized classpath resource location according to OS-specific file separator * @@ -96,8 +108,8 @@ public Path getFullTemplatePath(String name) { * @return A normalized string according to OS-specific file separator */ public static String getCPResourcePath(final String name) { - if (!"/".equals(File.separator)) { - return name.replaceAll(Pattern.quote(File.separator), "/"); + if (FILE_SEP_PATTERN != null) { + return FILE_SEP_PATTERN.matcher(name).replaceAll("/"); } return name; } @@ -262,6 +274,8 @@ private File writeToFileRaw(String filename, byte[] contents) throws IOException } private boolean filesEqual(File file1, File file2) throws IOException { - return file1.exists() && file2.exists() && Arrays.equals(Files.readAllBytes(file1.toPath()), Files.readAllBytes(file2.toPath())); + if (!file1.exists() || !file2.exists()) return false; + if (file1.length() != file2.length()) return false; + return Arrays.equals(Files.readAllBytes(file1.toPath()), Files.readAllBytes(file2.toPath())); } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java index ef1d8500b5b1..62b9ca91887a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/GeneratorTemplateContentLocator.java @@ -7,6 +7,8 @@ import java.io.File; import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; /** * Locates templates according to {@link CodegenConfig} settings. @@ -14,6 +16,13 @@ public class GeneratorTemplateContentLocator implements TemplatePathLocator { private final CodegenConfig codegenConfig; + /** + * Cache of relativeTemplateFile -> resolved full path (or empty Optional when the template does not exist). + * The filesystem/classpath existence probes inside resolveFullTemplatePath are expensive on repeated calls + * for the same template name, so we memoize the result for the lifetime of this locator instance. + */ + private final ConcurrentHashMap> templatePathCache = new ConcurrentHashMap<>(); + /** * Constructs a new instance of {@link GeneratorTemplateContentLocator} for the provided {@link CodegenConfig} * @@ -51,12 +60,25 @@ private boolean classpathTemplateExists(String name) { * 4) (embedded template dir) *

* Where "template dir" may be user defined and "embedded template dir" are the built-in templates for the given generator. + *

+ * Results are cached per {@code relativeTemplateFile} name because the filesystem/classpath probes are expensive + * and the outcome is constant for the lifetime of this locator instance. * * @param relativeTemplateFile Template file - * @return String Full template file path + * @return String Full template file path, or {@code null} if the template does not exist in any location */ @Override public String getFullTemplatePath(String relativeTemplateFile) { + return templatePathCache + .computeIfAbsent(relativeTemplateFile, key -> Optional.ofNullable(resolveFullTemplatePath(key))) + .orElse(null); + } + + /** + * Performs the actual filesystem/classpath probes to find the full template path. + * Called at most once per unique {@code relativeTemplateFile} value; all subsequent lookups use the cache. + */ + private String resolveFullTemplatePath(String relativeTemplateFile) { CodegenConfig config = this.codegenConfig; //check the supplied template library folder for the file diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java index 0805c267282d..f07006a4c13e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/HandlebarsEngineAdapter.java @@ -38,8 +38,8 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter { final Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class); @@ -48,7 +48,24 @@ public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter { // We use this as a simple lookup for valid file name extensions. This adapter will inspect .mustache (built-in) and infer the relevant handlebars filename private final String[] canCompileFromExtensions = {".handlebars", ".hbs", ".mustache"}; private boolean infiniteLoops = false; - @Setter private boolean prettyPrint = false; + @Setter + private boolean prettyPrint = false; + + /** + * Per-executor cache of fully-configured {@link Handlebars} engine instances. + * Each executor gets its own engine because the engine's {@link TemplateLoader} closes over the + * executor; sharing an engine across executors would silently resolve templates from the wrong source. + * {@link ConcurrentHashMap#computeIfAbsent} ensures the engine is built at most once per executor. + */ + private final ConcurrentHashMap engineCache = new ConcurrentHashMap<>(); + + /** + * Per-executor cache of compiled {@link Template} objects. + * Keying on the executor instance eliminates the non-atomic check-clear-update invalidation + * that the previous single-cache approach required; no state ever needs to be cleared. + */ + private final ConcurrentHashMap> templateCaches = + new ConcurrentHashMap<>(); /** * Provides an identifier used to load the adapter. This could be a name, uuid, or any other string. @@ -63,13 +80,6 @@ public String getIdentifier() { @Override public String compileTemplate(TemplatingExecutor executor, Map bundle, String templateFile) throws IOException { - TemplateLoader loader = new AbstractTemplateLoader() { - @Override - public TemplateSource sourceAt(String location) { - return findTemplate(executor, location); - } - }; - Context context = Context .newBuilder(bundle) .resolver( @@ -79,9 +89,33 @@ public TemplateSource sourceAt(String location) { AccessAwareFieldValueResolver.INSTANCE) .build(); + // Each executor gets its own Handlebars engine (the loader closes over the executor) and its + // own compiled-template cache. computeIfAbsent is atomic, so concurrent calls with the same + // executor share one engine/cache rather than racing to create duplicates. + Handlebars handlebars = engineCache.computeIfAbsent(executor, this::buildHandlebars); + ConcurrentHashMap cache = + templateCaches.computeIfAbsent(executor, k -> new ConcurrentHashMap<>()); + + // Manual get → compile → put so IOException propagates naturally. + Template tmpl = cache.get(templateFile); + if (tmpl == null) { + tmpl = handlebars.compile(templateFile); + cache.put(templateFile, tmpl); + } + return tmpl.apply(context); + } + + /** Constructs and fully configures a {@link Handlebars} engine for the given executor. */ + private Handlebars buildHandlebars(TemplatingExecutor executor) { + TemplateLoader loader = new AbstractTemplateLoader() { + @Override + public TemplateSource sourceAt(String location) { + return findTemplate(executor, location); + } + }; Handlebars handlebars = new Handlebars(loader); handlebars.registerHelperMissing((obj, options) -> { - LOGGER.warn(String.format(Locale.ROOT, "Unregistered helper name '%s', processing template:%n%s", options.helperName, options.fn.text())); + LOGGER.warn("Unregistered helper name '{}', processing template:\n{}", options.helperName, options.fn.text()); return ""; }); handlebars.registerHelper("json", Jackson2Helper.INSTANCE); @@ -90,8 +124,7 @@ public TemplateSource sourceAt(String location) { handlebars.registerHelpers(org.openapitools.codegen.templating.handlebars.StringHelpers.class); handlebars.setInfiniteLoops(infiniteLoops); handlebars.setPrettyPrint(prettyPrint); - Template tmpl = handlebars.compile(templateFile); - return tmpl.apply(context); + return handlebars; } @SuppressWarnings("java:S108") diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/MustacheEngineAdapter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/MustacheEngineAdapter.java index a71b71a985b0..fff6b7441ebf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/MustacheEngineAdapter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/MustacheEngineAdapter.java @@ -31,6 +31,7 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class MustacheEngineAdapter implements TemplatingEngineAdapter { @@ -51,6 +52,20 @@ public String getIdentifier() { @Getter @Setter Mustache.Compiler compiler = Mustache.compiler(); + /** + * Per-executor cache of template file name → compiled {@link Template}. + *

+ * Keying on the executor instance eliminates the non-atomic check-clear-update invalidation pattern + * that the previous single-cache approach required. Each executor gets its own independent inner + * map, so different executors (e.g. different generator runs, test fixtures) can never observe + * each other's compiled templates, and no state ever needs to be cleared. + *

+ * {@link ConcurrentHashMap#computeIfAbsent} guarantees that the inner map for a given executor + * is created exactly once even under concurrent access. + */ + private final ConcurrentHashMap> compiledTemplateCaches = + new ConcurrentHashMap<>(); + /** * Compiles a template into a string * @@ -62,10 +77,22 @@ public String getIdentifier() { */ @Override public String compileTemplate(TemplatingExecutor executor, Map bundle, String templateFile) throws IOException { - Template tmpl = compiler - .withLoader(name -> findTemplate(executor, name)) - .defaultValue("") - .compile(executor.getFullTemplateContents(templateFile)); + // Each executor gets its own compiled-template cache. computeIfAbsent is atomic, so two threads + // racing on the same executor key will share one inner map rather than creating two separate ones. + ConcurrentHashMap cache = + compiledTemplateCaches.computeIfAbsent(executor, k -> new ConcurrentHashMap<>()); + + // Manual get → compile → put so IOException propagates naturally. + // At worst, two threads compile the same template simultaneously; the last writer wins, + // which is harmless because compilation is pure/deterministic. + Template tmpl = cache.get(templateFile); + if (tmpl == null) { + tmpl = compiler + .withLoader(name -> findTemplate(executor, name)) + .defaultValue("") + .compile(executor.getFullTemplateContents(templateFile)); + cache.put(templateFile, tmpl); + } StringWriter out = new StringWriter(); // the value of bundle[MUSTACHE_PARENT_CONTEXT] is used a parent content in mustache.