diff --git a/modules/openapi-generator-gradle-plugin/build.gradle b/modules/openapi-generator-gradle-plugin/build.gradle index 9187ac861e19..3471d368438e 100644 --- a/modules/openapi-generator-gradle-plugin/build.gradle +++ b/modules/openapi-generator-gradle-plugin/build.gradle @@ -47,6 +47,7 @@ dependencies { tasks.named("test", Test).configure { useTestNG() testLogging.showStandardStreams = false + systemProperty("openApiGeneratorVersion", openApiGeneratorVersion) beforeTest { descriptor -> logger.lifecycle("Running test: " + descriptor) diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index 1b2d37526516..d79dc23fa116 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -62,6 +62,12 @@ class OpenApiGeneratorPlugin : Plugin { generate.outputDir.set(project.layout.buildDirectory.dir("generate-resources/main").map { it.asFile.path }) + val openApiGeneratorClasspath = configurations.create("openApiGeneratorClasspath") { + isCanBeConsumed = false + isCanBeResolved = true + description = "Extra jars for the OpenAPI Generator worker (custom generators)" + } + tasks.apply { register("openApiGenerators", GeneratorsTask::class.java).configure { group = pluginGroup @@ -93,6 +99,8 @@ class OpenApiGeneratorPlugin : Plugin { description = "Generate code via Open API Tools Generator for Open API 2.0 or 3.x specification documents." + generatorClasspath.setFrom(openApiGeneratorClasspath) + verbose.set(generate.verbose) validateSpec.set(generate.validateSpec) generatorName.set(generate.generatorName) @@ -126,6 +134,8 @@ class OpenApiGeneratorPlugin : Plugin { nameMappings.set(generate.nameMappings) modelNameMappings.set(generate.modelNameMappings) parameterNameMappings.set(generate.parameterNameMappings) + enumNameMappings.set(generate.enumNameMappings) + operationIdNameMappings.set(generate.operationIdNameMappings) openapiNormalizer.set(generate.openapiNormalizer) invokerPackage.set(generate.invokerPackage) groupId.set(generate.groupId) diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index 61b66a1d409a..7c495ca37123 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -20,10 +20,12 @@ import javax.inject.Inject import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileSystemOperations import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFile @@ -40,11 +42,7 @@ import org.gradle.kotlin.dsl.listProperty import org.gradle.kotlin.dsl.mapProperty import org.gradle.kotlin.dsl.property import org.gradle.util.GradleVersion -import org.openapitools.codegen.CodegenConstants -import org.openapitools.codegen.DefaultGenerator -import org.openapitools.codegen.config.CodegenConfigurator -import org.openapitools.codegen.config.GlobalSettings -import org.openapitools.codegen.config.MergedSpecBuilder +import org.gradle.workers.WorkerExecutor /** * A task which generates the desired code. @@ -56,7 +54,20 @@ import org.openapitools.codegen.config.MergedSpecBuilder * @author Jim Schubert */ @CacheableTask -open class GenerateTask @Inject constructor(private val objectFactory: ObjectFactory) : DefaultTask() { +open class GenerateTask @Inject constructor( + private val objectFactory: ObjectFactory, + private val workerExecutor: WorkerExecutor +) : DefaultTask() { + + /** + * Extra classpath entries for the code generation worker. + * Add custom generator jars via the `openApiGeneratorClasspath` configuration. + * Parent-first classloader delegation means these entries supplement, + * not override, the plugin's own classpath. + */ + @get:Optional + @get:Classpath + val generatorClasspath: ConfigurableFileCollection = project.objects.fileCollection() /** * The verbosity of generation @@ -606,8 +617,6 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac } } - protected open fun createDefaultCodegenConfigurator(): CodegenConfigurator = CodegenConfigurator() - private fun createFileSystemManager(): FileSystemManager { return if(GradleVersion.current() >= GradleVersion.version("6.0")) { objectFactory.newInstance(FileSystemManagerDefault::class.java) @@ -619,30 +628,6 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @Suppress("unused") @TaskAction fun doWork() { - var resolvedInputSpec = "" - - inputSpec.ifNotEmpty { value -> - resolvedInputSpec = value - } - - remoteInputSpec.ifNotEmpty { value -> - resolvedInputSpec = value - } - - inputSpecRootDirectory.ifNotEmpty { inputSpecRootDirectoryValue -> - val skipMerge = inputSpecRootDirectorySkipMerge.get() - val runMergeSpec = !skipMerge - if (runMergeSpec) { - run { - resolvedInputSpec = MergedSpecBuilder( - inputSpecRootDirectoryValue, - mergedFileName.getOrElse("merged") - ).buildMergedSpec() - logger.info("Merge input spec would be used - {}", resolvedInputSpec) - } - } - } - cleanupOutput.ifNotEmpty { cleanup -> if (cleanup) { createFileSystemManager().delete(outputDir) @@ -652,334 +637,99 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac } } - val configurator: CodegenConfigurator = if (configFile.isPresent) { - CodegenConfigurator.fromFile(configFile.get()) - } else createDefaultCodegenConfigurator() - - try { - if (globalProperties.isPresent) { - globalProperties.get().forEach { (key, value) -> - configurator.addGlobalProperty(key, value) - } - } - - if (supportingFilesConstrainedTo.isPresent && supportingFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty( - CodegenConstants.SUPPORTING_FILES, - supportingFilesConstrainedTo.get().joinToString(",") - ) - } else { - GlobalSettings.clearProperty(CodegenConstants.SUPPORTING_FILES) - } - - if (modelFilesConstrainedTo.isPresent && modelFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty(CodegenConstants.MODELS, modelFilesConstrainedTo.get().joinToString(",")) - } else { - GlobalSettings.clearProperty(CodegenConstants.MODELS) - } - - if (apiFilesConstrainedTo.isPresent && apiFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty(CodegenConstants.APIS, apiFilesConstrainedTo.get().joinToString(",")) - } else { - GlobalSettings.clearProperty(CodegenConstants.APIS) - } - - if (generateApiDocumentation.isPresent) { - GlobalSettings.setProperty(CodegenConstants.API_DOCS, generateApiDocumentation.get().toString()) - } - - if (generateModelDocumentation.isPresent) { - GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, generateModelDocumentation.get().toString()) - } - - if (generateModelTests.isPresent) { - GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, generateModelTests.get().toString()) - } - - if (generateApiTests.isPresent) { - GlobalSettings.setProperty(CodegenConstants.API_TESTS, generateApiTests.get().toString()) - } - - if (inputSpec.isPresent && remoteInputSpec.isPresent) { - logger.warn("Both inputSpec and remoteInputSpec is specified. The remoteInputSpec will take priority over inputSpec.") - } - - configurator.setInputSpec(resolvedInputSpec) - - // now override with any specified parameters - verbose.ifNotEmpty { value -> - configurator.setVerbose(value) - } - - validateSpec.ifNotEmpty { value -> - configurator.setValidateSpec(value) - } - - skipOverwrite.ifNotEmpty { value -> - configurator.setSkipOverwrite(value) - } - - generatorName.ifNotEmpty { value -> - configurator.setGeneratorName(value) - } - - outputDir.ifNotEmpty { value -> - configurator.setOutputDir(value) - } - - auth.ifNotEmpty { value -> - configurator.setAuth(value) - } - - templateDir.ifNotEmpty { value -> - configurator.setTemplateDir(value) - } - - templateResourcePath.ifNotEmpty { value -> - templateDir.ifNotEmpty { - logger.warn("Both templateDir and templateResourcePath were configured. templateResourcePath overwrites templateDir.") - } - configurator.setTemplateDir(value) - } - - packageName.ifNotEmpty { value -> - configurator.setPackageName(value) - } - - apiPackage.ifNotEmpty { value -> - configurator.setApiPackage(value) - } - - modelPackage.ifNotEmpty { value -> - configurator.setModelPackage(value) - } - - modelNamePrefix.ifNotEmpty { value -> - configurator.setModelNamePrefix(value) - } - - modelNameSuffix.ifNotEmpty { value -> - configurator.setModelNameSuffix(value) - } - - apiNameSuffix.ifNotEmpty { value -> - configurator.setApiNameSuffix(value) - } - - invokerPackage.ifNotEmpty { value -> - configurator.setInvokerPackage(value) - } - - groupId.ifNotEmpty { value -> - configurator.setGroupId(value) - } - - id.ifNotEmpty { value -> - configurator.setArtifactId(value) - } - - version.ifNotEmpty { value -> - configurator.setArtifactVersion(value) - } - - library.ifNotEmpty { value -> - configurator.setLibrary(value) - } - - gitHost.ifNotEmpty { value -> - configurator.setGitHost(value) - } - - gitUserId.ifNotEmpty { value -> - configurator.setGitUserId(value) - } - - gitRepoId.ifNotEmpty { value -> - configurator.setGitRepoId(value) - } - - releaseNote.ifNotEmpty { value -> - configurator.setReleaseNote(value) - } - - httpUserAgent.ifNotEmpty { value -> - configurator.setHttpUserAgent(value) - } - - ignoreFileOverride.ifNotEmpty { value -> - configurator.setIgnoreFileOverride(value) - } - - removeOperationIdPrefix.ifNotEmpty { value -> - configurator.setRemoveOperationIdPrefix(value) - } - - skipOperationExample.ifNotEmpty { value -> - configurator.setSkipOperationExample(value) - } - - logToStderr.ifNotEmpty { value -> - configurator.setLogToStderr(value) - } - - enablePostProcessFile.ifNotEmpty { value -> - configurator.setEnablePostProcessFile(value) - } - - skipValidateSpec.ifNotEmpty { value -> - configurator.setValidateSpec(!value) - } - - generateAliasAsModel.ifNotEmpty { value -> - configurator.setGenerateAliasAsModel(value) - } - - engine.ifNotEmpty { value -> - if ("handlebars".equals(value, ignoreCase = true)) { - configurator.setTemplatingEngineName("handlebars") - } else { - configurator.setTemplatingEngineName(value) - } - } - - if (globalProperties.isPresent) { - globalProperties.get().forEach { entry -> - configurator.addGlobalProperty(entry.key, entry.value) - } - } - - if (instantiationTypes.isPresent) { - instantiationTypes.get().forEach { entry -> - configurator.addInstantiationType(entry.key, entry.value) - } - } - - if (importMappings.isPresent) { - importMappings.get().forEach { entry -> - configurator.addImportMapping(entry.key, entry.value) - } - } - - if (schemaMappings.isPresent) { - schemaMappings.get().forEach { entry -> - configurator.addSchemaMapping(entry.key, entry.value) - } - } - - if (inlineSchemaNameMappings.isPresent) { - inlineSchemaNameMappings.get().forEach { entry -> - configurator.addInlineSchemaNameMapping(entry.key, entry.value) - } - } - - if (inlineSchemaOptions.isPresent) { - inlineSchemaOptions.get().forEach { entry -> - configurator.addInlineSchemaOption(entry.key, entry.value) - } - } - - if (nameMappings.isPresent) { - nameMappings.get().forEach { entry -> - configurator.addNameMapping(entry.key, entry.value) - } - } - - if (parameterNameMappings.isPresent) { - parameterNameMappings.get().forEach { entry -> - configurator.addParameterNameMapping(entry.key, entry.value) - } - } - - if (modelNameMappings.isPresent) { - modelNameMappings.get().forEach { entry -> - configurator.addModelNameMapping(entry.key, entry.value) - } - } - - if (enumNameMappings.isPresent) { - enumNameMappings.get().forEach { entry -> - configurator.addEnumNameMapping(entry.key, entry.value) - } - } - - if (operationIdNameMappings.isPresent) { - operationIdNameMappings.get().forEach { entry -> - configurator.addOperationIdNameMapping(entry.key, entry.value) - } - } - - if (openapiNormalizer.isPresent) { - openapiNormalizer.get().forEach { entry -> - configurator.addOpenapiNormalizer(entry.key, entry.value) - } - } - - if (typeMappings.isPresent) { - typeMappings.get().forEach { entry -> - configurator.addTypeMapping(entry.key, entry.value) - } - } - - if (additionalProperties.isPresent) { - additionalProperties.get().forEach { entry -> - configurator.addAdditionalProperty(entry.key, entry.value) - } - } - - if (serverVariables.isPresent) { - serverVariables.get().forEach { entry -> - configurator.addServerVariable(entry.key, entry.value) - } - } - - if (languageSpecificPrimitives.isPresent) { - languageSpecificPrimitives.get().forEach { - configurator.addLanguageSpecificPrimitive(it) - } - } - - if (openapiGeneratorIgnoreList.isPresent) { - openapiGeneratorIgnoreList.get().forEach { - configurator.addOpenapiGeneratorIgnoreList(it) - } - } - - if (reservedWordsMappings.isPresent) { - reservedWordsMappings.get().forEach { entry -> - configurator.addAdditionalReservedWordMapping(entry.key, entry.value) - } - } - - var dryRunSetting = false - dryRun.ifNotEmpty { setting -> - dryRunSetting = setting - } - - val clientOptInput = configurator.toClientOptInput() - val codegenConfig = clientOptInput.config + // classLoaderIsolation prevents Jackson version conflicts with other + // plugins (issue #18753). The isolated classloader inherits the plugin's + // own classpath (openapi-generator and its dependencies). Extra entries + // from openApiGeneratorClasspath, if configured, supply custom generators. + val taskRef = this + val extraClasspath = taskRef.generatorClasspath.files + if (extraClasspath.isNotEmpty()) { + logger.info("OpenAPI Generator: adding {} extra classpath entries from openApiGeneratorClasspath", extraClasspath.size) + } - if (configOptions.isPresent) { - val userSpecifiedConfigOptions = configOptions.get() - codegenConfig.cliOptions().forEach { - if (userSpecifiedConfigOptions.containsKey(it.opt)) { - clientOptInput.config.additionalProperties()[it.opt] = userSpecifiedConfigOptions[it.opt] - } - } + val workQueue = workerExecutor.classLoaderIsolation { + if (extraClasspath.isNotEmpty()) { + classpath.from(extraClasspath) } + } - try { - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - out.withStyle(StyledTextOutput.Style.Success) - - DefaultGenerator(dryRunSetting).opts(clientOptInput).generate() + workQueue.submit(GenerateWorkAction::class.java) { + inputSpec.set(taskRef.inputSpec) + inputSpecRootDirectory.set(taskRef.inputSpecRootDirectory) + inputSpecRootDirectorySkipMerge.set(taskRef.inputSpecRootDirectorySkipMerge) + mergedFileName.set(taskRef.mergedFileName) + remoteInputSpec.set(taskRef.remoteInputSpec) + verbose.set(taskRef.verbose) + validateSpec.set(taskRef.validateSpec) + generatorName.set(taskRef.generatorName) + outputDir.set(taskRef.outputDir) + templateDir.set(taskRef.templateDir) + templateResourcePath.set(taskRef.templateResourcePath) + auth.set(taskRef.auth) + globalProperties.set(taskRef.globalProperties) + configFile.set(taskRef.configFile) + skipOverwrite.set(taskRef.skipOverwrite) + packageName.set(taskRef.packageName) + apiPackage.set(taskRef.apiPackage) + modelPackage.set(taskRef.modelPackage) + modelNamePrefix.set(taskRef.modelNamePrefix) + modelNameSuffix.set(taskRef.modelNameSuffix) + apiNameSuffix.set(taskRef.apiNameSuffix) + instantiationTypes.set(taskRef.instantiationTypes) + typeMappings.set(taskRef.typeMappings) + additionalProperties.set(taskRef.additionalProperties) + serverVariables.set(taskRef.serverVariables) + languageSpecificPrimitives.set(taskRef.languageSpecificPrimitives) + openapiGeneratorIgnoreList.set(taskRef.openapiGeneratorIgnoreList) + importMappings.set(taskRef.importMappings) + schemaMappings.set(taskRef.schemaMappings) + inlineSchemaNameMappings.set(taskRef.inlineSchemaNameMappings) + inlineSchemaOptions.set(taskRef.inlineSchemaOptions) + nameMappings.set(taskRef.nameMappings) + parameterNameMappings.set(taskRef.parameterNameMappings) + modelNameMappings.set(taskRef.modelNameMappings) + enumNameMappings.set(taskRef.enumNameMappings) + operationIdNameMappings.set(taskRef.operationIdNameMappings) + openapiNormalizer.set(taskRef.openapiNormalizer) + invokerPackage.set(taskRef.invokerPackage) + groupId.set(taskRef.groupId) + id.set(taskRef.id) + version.set(taskRef.version) + library.set(taskRef.library) + gitHost.set(taskRef.gitHost) + gitUserId.set(taskRef.gitUserId) + gitRepoId.set(taskRef.gitRepoId) + releaseNote.set(taskRef.releaseNote) + httpUserAgent.set(taskRef.httpUserAgent) + reservedWordsMappings.set(taskRef.reservedWordsMappings) + ignoreFileOverride.set(taskRef.ignoreFileOverride) + removeOperationIdPrefix.set(taskRef.removeOperationIdPrefix) + skipOperationExample.set(taskRef.skipOperationExample) + apiFilesConstrainedTo.set(taskRef.apiFilesConstrainedTo) + modelFilesConstrainedTo.set(taskRef.modelFilesConstrainedTo) + supportingFilesConstrainedTo.set(taskRef.supportingFilesConstrainedTo) + generateModelTests.set(taskRef.generateModelTests) + generateModelDocumentation.set(taskRef.generateModelDocumentation) + generateApiTests.set(taskRef.generateApiTests) + generateApiDocumentation.set(taskRef.generateApiDocumentation) + configOptions.set(taskRef.configOptions) + logToStderr.set(taskRef.logToStderr) + enablePostProcessFile.set(taskRef.enablePostProcessFile) + skipValidateSpec.set(taskRef.skipValidateSpec) + generateAliasAsModel.set(taskRef.generateAliasAsModel) + engine.set(taskRef.engine) + dryRun.set(taskRef.dryRun) + } - out.println("Successfully generated code to ${outputDir.get()}") - } catch (e: RuntimeException) { - throw GradleException("Code generation failed.", e) - } - } finally { - GlobalSettings.reset() + try { + workQueue.await() + } catch (e: Exception) { + throw GradleException("Code generation failed. See the worker output above for details.", e) } + + val out = services.get(StyledTextOutputFactory::class.java).create("openapi") + out.withStyle(StyledTextOutput.Style.Success) + out.println("Successfully generated code to ${outputDir.get()}") } } diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkAction.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkAction.kt new file mode 100644 index 000000000000..0a6846ed2fda --- /dev/null +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkAction.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.generator.gradle.plugin.tasks + +import org.gradle.api.provider.Property +import org.gradle.workers.WorkAction +import org.openapitools.codegen.CodegenConstants +import org.openapitools.codegen.DefaultGenerator +import org.openapitools.codegen.config.CodegenConfigurator +import org.openapitools.codegen.config.GlobalSettings +import org.openapitools.codegen.config.MergedSpecBuilder +import org.slf4j.LoggerFactory + +/** + * Executes OpenAPI code generation as a Gradle [WorkAction]. + * + * [GenerateTask] submits this action via + * [org.gradle.workers.WorkerExecutor.classLoaderIsolation] to prevent + * Jackson version conflicts with other Gradle plugins (see issue #18753). + * The isolated classloader inherits the plugin's own classpath, including + * openapi-generator and its correct Jackson version. + */ +abstract class GenerateWorkAction : WorkAction { + + private val logger = LoggerFactory.getLogger(GenerateWorkAction::class.java) + + override fun execute() { + var resolvedInputSpec = "" + + parameters.inputSpec.ifNotEmpty { value -> + resolvedInputSpec = value + } + + parameters.remoteInputSpec.ifNotEmpty { value -> + resolvedInputSpec = value + } + + parameters.inputSpecRootDirectory.ifNotEmpty { inputSpecRootDirectoryValue -> + val skipMerge = parameters.inputSpecRootDirectorySkipMerge.getOrElse(false) + if (!skipMerge) { + resolvedInputSpec = MergedSpecBuilder( + inputSpecRootDirectoryValue, + parameters.mergedFileName.getOrElse("merged") + ).buildMergedSpec() + logger.info("Merge input spec would be used - {}", resolvedInputSpec) + } + } + + val configurator: CodegenConfigurator = if (parameters.configFile.isPresent) { + CodegenConfigurator.fromFile(parameters.configFile.get()) + } else { + CodegenConfigurator() + } + + try { + if (parameters.globalProperties.isPresent) { + parameters.globalProperties.get().forEach { (key, value) -> + configurator.addGlobalProperty(key, value) + } + } + + if (parameters.supportingFilesConstrainedTo.isPresent && parameters.supportingFilesConstrainedTo.get().isNotEmpty()) { + GlobalSettings.setProperty( + CodegenConstants.SUPPORTING_FILES, + parameters.supportingFilesConstrainedTo.get().joinToString(",") + ) + } else { + GlobalSettings.clearProperty(CodegenConstants.SUPPORTING_FILES) + } + + if (parameters.modelFilesConstrainedTo.isPresent && parameters.modelFilesConstrainedTo.get().isNotEmpty()) { + GlobalSettings.setProperty(CodegenConstants.MODELS, parameters.modelFilesConstrainedTo.get().joinToString(",")) + } else { + GlobalSettings.clearProperty(CodegenConstants.MODELS) + } + + if (parameters.apiFilesConstrainedTo.isPresent && parameters.apiFilesConstrainedTo.get().isNotEmpty()) { + GlobalSettings.setProperty(CodegenConstants.APIS, parameters.apiFilesConstrainedTo.get().joinToString(",")) + } else { + GlobalSettings.clearProperty(CodegenConstants.APIS) + } + + if (parameters.generateApiDocumentation.isPresent) { + GlobalSettings.setProperty(CodegenConstants.API_DOCS, parameters.generateApiDocumentation.get().toString()) + } + + if (parameters.generateModelDocumentation.isPresent) { + GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, parameters.generateModelDocumentation.get().toString()) + } + + if (parameters.generateModelTests.isPresent) { + GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, parameters.generateModelTests.get().toString()) + } + + if (parameters.generateApiTests.isPresent) { + GlobalSettings.setProperty(CodegenConstants.API_TESTS, parameters.generateApiTests.get().toString()) + } + + if (parameters.inputSpec.isPresent && parameters.remoteInputSpec.isPresent) { + logger.warn("Both inputSpec and remoteInputSpec is specified. The remoteInputSpec will take priority over inputSpec.") + } + + configurator.setInputSpec(resolvedInputSpec) + + parameters.verbose.ifNotEmpty { value -> configurator.setVerbose(value) } + parameters.validateSpec.ifNotEmpty { value -> configurator.setValidateSpec(value) } + parameters.skipOverwrite.ifNotEmpty { value -> configurator.setSkipOverwrite(value) } + parameters.generatorName.ifNotEmpty { value -> configurator.setGeneratorName(value) } + parameters.outputDir.ifNotEmpty { value -> configurator.setOutputDir(value) } + parameters.auth.ifNotEmpty { value -> configurator.setAuth(value) } + + parameters.templateDir.ifNotEmpty { value -> configurator.setTemplateDir(value) } + parameters.templateResourcePath.ifNotEmpty { value -> + parameters.templateDir.ifNotEmpty { + logger.warn("Both templateDir and templateResourcePath were configured. templateResourcePath overwrites templateDir.") + } + configurator.setTemplateDir(value) + } + + parameters.packageName.ifNotEmpty { value -> configurator.setPackageName(value) } + parameters.apiPackage.ifNotEmpty { value -> configurator.setApiPackage(value) } + parameters.modelPackage.ifNotEmpty { value -> configurator.setModelPackage(value) } + parameters.modelNamePrefix.ifNotEmpty { value -> configurator.setModelNamePrefix(value) } + parameters.modelNameSuffix.ifNotEmpty { value -> configurator.setModelNameSuffix(value) } + parameters.apiNameSuffix.ifNotEmpty { value -> configurator.setApiNameSuffix(value) } + parameters.invokerPackage.ifNotEmpty { value -> configurator.setInvokerPackage(value) } + parameters.groupId.ifNotEmpty { value -> configurator.setGroupId(value) } + parameters.id.ifNotEmpty { value -> configurator.setArtifactId(value) } + parameters.version.ifNotEmpty { value -> configurator.setArtifactVersion(value) } + parameters.library.ifNotEmpty { value -> configurator.setLibrary(value) } + parameters.gitHost.ifNotEmpty { value -> configurator.setGitHost(value) } + parameters.gitUserId.ifNotEmpty { value -> configurator.setGitUserId(value) } + parameters.gitRepoId.ifNotEmpty { value -> configurator.setGitRepoId(value) } + parameters.releaseNote.ifNotEmpty { value -> configurator.setReleaseNote(value) } + parameters.httpUserAgent.ifNotEmpty { value -> configurator.setHttpUserAgent(value) } + parameters.ignoreFileOverride.ifNotEmpty { value -> configurator.setIgnoreFileOverride(value) } + parameters.removeOperationIdPrefix.ifNotEmpty { value -> configurator.setRemoveOperationIdPrefix(value) } + parameters.skipOperationExample.ifNotEmpty { value -> configurator.setSkipOperationExample(value) } + parameters.logToStderr.ifNotEmpty { value -> configurator.setLogToStderr(value) } + parameters.enablePostProcessFile.ifNotEmpty { value -> configurator.setEnablePostProcessFile(value) } + + parameters.skipValidateSpec.ifNotEmpty { value -> configurator.setValidateSpec(!value) } + parameters.generateAliasAsModel.ifNotEmpty { value -> configurator.setGenerateAliasAsModel(value) } + + parameters.engine.ifNotEmpty { value -> + if ("handlebars".equals(value, ignoreCase = true)) { + configurator.setTemplatingEngineName("handlebars") + } else { + configurator.setTemplatingEngineName(value) + } + } + + if (parameters.instantiationTypes.isPresent) { + parameters.instantiationTypes.get().forEach { entry -> + configurator.addInstantiationType(entry.key, entry.value) + } + } + + if (parameters.importMappings.isPresent) { + parameters.importMappings.get().forEach { entry -> + configurator.addImportMapping(entry.key, entry.value) + } + } + + if (parameters.schemaMappings.isPresent) { + parameters.schemaMappings.get().forEach { entry -> + configurator.addSchemaMapping(entry.key, entry.value) + } + } + + if (parameters.inlineSchemaNameMappings.isPresent) { + parameters.inlineSchemaNameMappings.get().forEach { entry -> + configurator.addInlineSchemaNameMapping(entry.key, entry.value) + } + } + + if (parameters.inlineSchemaOptions.isPresent) { + parameters.inlineSchemaOptions.get().forEach { entry -> + configurator.addInlineSchemaOption(entry.key, entry.value) + } + } + + if (parameters.nameMappings.isPresent) { + parameters.nameMappings.get().forEach { entry -> + configurator.addNameMapping(entry.key, entry.value) + } + } + + if (parameters.parameterNameMappings.isPresent) { + parameters.parameterNameMappings.get().forEach { entry -> + configurator.addParameterNameMapping(entry.key, entry.value) + } + } + + if (parameters.modelNameMappings.isPresent) { + parameters.modelNameMappings.get().forEach { entry -> + configurator.addModelNameMapping(entry.key, entry.value) + } + } + + if (parameters.enumNameMappings.isPresent) { + parameters.enumNameMappings.get().forEach { entry -> + configurator.addEnumNameMapping(entry.key, entry.value) + } + } + + if (parameters.operationIdNameMappings.isPresent) { + parameters.operationIdNameMappings.get().forEach { entry -> + configurator.addOperationIdNameMapping(entry.key, entry.value) + } + } + + if (parameters.openapiNormalizer.isPresent) { + parameters.openapiNormalizer.get().forEach { entry -> + configurator.addOpenapiNormalizer(entry.key, entry.value) + } + } + + if (parameters.typeMappings.isPresent) { + parameters.typeMappings.get().forEach { entry -> + configurator.addTypeMapping(entry.key, entry.value) + } + } + + if (parameters.additionalProperties.isPresent) { + parameters.additionalProperties.get().forEach { entry -> + configurator.addAdditionalProperty(entry.key, entry.value) + } + } + + if (parameters.serverVariables.isPresent) { + parameters.serverVariables.get().forEach { entry -> + configurator.addServerVariable(entry.key, entry.value) + } + } + + if (parameters.languageSpecificPrimitives.isPresent) { + parameters.languageSpecificPrimitives.get().forEach { + configurator.addLanguageSpecificPrimitive(it) + } + } + + if (parameters.openapiGeneratorIgnoreList.isPresent) { + parameters.openapiGeneratorIgnoreList.get().forEach { + configurator.addOpenapiGeneratorIgnoreList(it) + } + } + + if (parameters.reservedWordsMappings.isPresent) { + parameters.reservedWordsMappings.get().forEach { entry -> + configurator.addAdditionalReservedWordMapping(entry.key, entry.value) + } + } + + val dryRunSetting = parameters.dryRun.getOrElse(false) + + val clientOptInput = configurator.toClientOptInput() + + if (parameters.configOptions.isPresent) { + val userSpecifiedConfigOptions = parameters.configOptions.get() + clientOptInput.config.cliOptions().forEach { + if (userSpecifiedConfigOptions.containsKey(it.opt)) { + clientOptInput.config.additionalProperties()[it.opt] = userSpecifiedConfigOptions[it.opt] + } + } + } + + try { + DefaultGenerator(dryRunSetting).opts(clientOptInput).generate() + } catch (e: RuntimeException) { + throw RuntimeException("Code generation failed.", e) + } + } finally { + GlobalSettings.reset() + } + } + + /** + * Mirrors the `ifNotEmpty` helper from [GenerateTask] for [WorkParameters] properties. + */ + private fun Property.ifNotEmpty(block: Property.(T) -> Unit) { + if (isPresent) { + when (val value = get()) { + is String -> if (value.isNotEmpty()) block(value) + else -> block(value) + } + } + } +} diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkParameters.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkParameters.kt new file mode 100644 index 000000000000..ee56280829ad --- /dev/null +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateWorkParameters.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.generator.gradle.plugin.tasks + +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.workers.WorkParameters + +/** + * Parameters for [GenerateWorkAction]. + * + * Each property mirrors a field on [GenerateTask]. The task wires values + * before submitting work to the worker. + */ +interface GenerateWorkParameters : WorkParameters { + // Input spec (resolved by the task before submission) + val inputSpec: Property + val inputSpecRootDirectory: Property + val inputSpecRootDirectorySkipMerge: Property + val mergedFileName: Property + val remoteInputSpec: Property + + // Generator settings + val verbose: Property + val validateSpec: Property + val generatorName: Property + val outputDir: Property + val templateDir: Property + val templateResourcePath: Property + val auth: Property + val configFile: Property + val skipOverwrite: Property + val packageName: Property + val apiPackage: Property + val modelPackage: Property + val modelNamePrefix: Property + val modelNameSuffix: Property + val apiNameSuffix: Property + val invokerPackage: Property + val groupId: Property + val id: Property + val version: Property + val library: Property + val gitHost: Property + val gitUserId: Property + val gitRepoId: Property + val releaseNote: Property + val httpUserAgent: Property + val ignoreFileOverride: Property + val removeOperationIdPrefix: Property + val skipOperationExample: Property + val logToStderr: Property + val enablePostProcessFile: Property + val skipValidateSpec: Property + val generateAliasAsModel: Property + val engine: Property + val dryRun: Property + + // Generation scope + val generateModelTests: Property + val generateModelDocumentation: Property + val generateApiTests: Property + val generateApiDocumentation: Property + val apiFilesConstrainedTo: ListProperty + val modelFilesConstrainedTo: ListProperty + val supportingFilesConstrainedTo: ListProperty + val openapiGeneratorIgnoreList: ListProperty + val languageSpecificPrimitives: ListProperty + + // Maps + val globalProperties: MapProperty + val instantiationTypes: MapProperty + val typeMappings: MapProperty + val additionalProperties: MapProperty + val serverVariables: MapProperty + val importMappings: MapProperty + val schemaMappings: MapProperty + val inlineSchemaNameMappings: MapProperty + val inlineSchemaOptions: MapProperty + val nameMappings: MapProperty + val parameterNameMappings: MapProperty + val modelNameMappings: MapProperty + val enumNameMappings: MapProperty + val operationIdNameMappings: MapProperty + val openapiNormalizer: MapProperty + val reservedWordsMappings: MapProperty + val configOptions: MapProperty +} diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskWorkerApiTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskWorkerApiTest.kt new file mode 100644 index 000000000000..c33e6421e5a1 --- /dev/null +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskWorkerApiTest.kt @@ -0,0 +1,110 @@ +package org.openapitools.generator.gradle.plugin + +import org.gradle.testkit.runner.TaskOutcome +import org.testng.annotations.Test +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GenerateTaskWorkerApiTest : TestBase() { + override var temp: File = File(System.getProperty("java.io.tmpdir"), javaClass.simpleName) + + /** + * Extra dependencies in `openApiGeneratorClasspath` reach the worker + * and appear in the log as extra classpath entries. + */ + @Test + fun `openApiGenerate should pass extra classpath entries to worker when openApiGeneratorClasspath is configured`() { + // Arrange + val projectFiles = mapOf( + "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml")!! + ) + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + openApiGeneratorClasspath("org.openapitools:openapi-generator:${openApiGeneratorVersion()}") + } + openApiGenerate { + generatorName = "kotlin" + inputSpec = file("spec.yaml").absolutePath + outputDir = file("build/kotlin").absolutePath + } + """.trimIndent() + withProject(buildContents, projectFiles) + + // Act + val result = build { + withArguments("openApiGenerate", "--stacktrace", "--info") + } + + // Assert + assertTrue( + result.output.contains("Successfully generated code to"), + "Generation should succeed when openApiGeneratorClasspath is explicitly configured" + ) + assertEquals( + TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome, + "Expected SUCCESS but got ${result.task(":openApiGenerate")?.outcome}" + ) + assertTrue( + File(temp, "build/kotlin/src/main/kotlin").exists(), + "Generated sources should exist" + ) + assertTrue( + result.output.contains("extra classpath entries"), + "Should log extra classpath entries when openApiGeneratorClasspath is configured" + ) + } + + /** + * Generation succeeds through the WorkAction path without explicit + * classpath configuration, exercising the full property wiring from + * GenerateTask through GenerateWorkParameters to GenerateWorkAction. + */ + @Test + fun `openApiGenerate should succeed through WorkAction without extra classpath configuration`() { + // Arrange + val projectFiles = mapOf( + "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml")!! + ) + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + openApiGenerate { + generatorName = "kotlin" + inputSpec = file("spec.yaml").absolutePath + outputDir = file("build/kotlin").absolutePath + } + """.trimIndent() + withProject(buildContents, projectFiles) + + // Act + val result = build { + withArguments("openApiGenerate", "--stacktrace") + } + + // Assert + assertTrue( + result.output.contains("Successfully generated code to"), + "Generation should succeed with automatic classpath isolation" + ) + assertEquals( + TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome, + "Expected SUCCESS but got ${result.task(":openApiGenerate")?.outcome}" + ) + assertTrue( + File(temp, "build/kotlin/src/main/kotlin").exists(), + "Generated sources should exist" + ) + } + + private fun openApiGeneratorVersion(): String = + System.getProperty("openApiGeneratorVersion", "7.21.0-SNAPSHOT") +}