All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openapitools.codegen.DefaultCodegen Maven / Gradle / Ivy

There is a newer version: 7.6.0
Show newest version
/*
 * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
 * Copyright 2018 SmartBear Software
 *
 * 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.codegen;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableMap;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Compiler;
import com.samskivert.mustache.Mustache.Lambda;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.*;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.servers.ServerVariable;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.text.StringEscapeUtils;
import org.openapitools.codegen.CodegenDiscriminator.MappedModel;
import org.openapitools.codegen.api.TemplatingEngineAdapter;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.examples.ExampleGenerator;
import org.openapitools.codegen.languages.PhpNextgenClientCodegen;
import org.openapitools.codegen.languages.RustServerCodegen;
import org.openapitools.codegen.meta.FeatureSet;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.model.WebhooksMap;
import org.openapitools.codegen.serializer.SerializerUtils;
import org.openapitools.codegen.templating.MustacheEngineAdapter;
import org.openapitools.codegen.templating.mustache.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.OneOfImplementorAdditionalData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openapitools.codegen.CodegenConstants.UNSUPPORTED_V310_SPEC_MSG;
import static org.openapitools.codegen.utils.OnceLogger.once;
import static org.openapitools.codegen.utils.StringUtils.*;

public class DefaultCodegen implements CodegenConfig {
    private final Logger LOGGER = LoggerFactory.getLogger(DefaultCodegen.class);

    public static FeatureSet DefaultFeatureSet;

    // A cache of sanitized words. The sanitizeName() method is invoked many times with the same
    // arguments, this cache is used to optimized performance.
    private static final Cache sanitizedNameCache;
    private static final String xSchemaTestExamplesKey = "x-schema-test-examples";
    private static final String xSchemaTestExamplesRefPrefix = "#/components/x-schema-test-examples/";
    protected static Schema falseSchema;
    protected static Schema trueSchema = new Schema();

    static {
        DefaultFeatureSet = FeatureSet.newBuilder()
                .includeDataTypeFeatures(
                        DataTypeFeature.Int32, DataTypeFeature.Int64, DataTypeFeature.Float, DataTypeFeature.Double,
                        DataTypeFeature.Decimal, DataTypeFeature.String, DataTypeFeature.Byte, DataTypeFeature.Binary,
                        DataTypeFeature.Boolean, DataTypeFeature.Date, DataTypeFeature.DateTime, DataTypeFeature.Password,
                        DataTypeFeature.File, DataTypeFeature.Array, DataTypeFeature.Object, DataTypeFeature.Maps, DataTypeFeature.CollectionFormat,
                        DataTypeFeature.CollectionFormatMulti, DataTypeFeature.Enum, DataTypeFeature.ArrayOfEnum, DataTypeFeature.ArrayOfModel,
                        DataTypeFeature.ArrayOfCollectionOfPrimitives, DataTypeFeature.ArrayOfCollectionOfModel, DataTypeFeature.ArrayOfCollectionOfEnum,
                        DataTypeFeature.MapOfEnum, DataTypeFeature.MapOfModel, DataTypeFeature.MapOfCollectionOfPrimitives,
                        DataTypeFeature.MapOfCollectionOfModel, DataTypeFeature.MapOfCollectionOfEnum
                        // Custom types are template specific
                )
                .includeDocumentationFeatures(
                        DocumentationFeature.Api, DocumentationFeature.Model
                        // README is template specific
                )
                .includeGlobalFeatures(
                        GlobalFeature.Host, GlobalFeature.BasePath, GlobalFeature.Info, GlobalFeature.PartialSchemes,
                        GlobalFeature.Consumes, GlobalFeature.Produces, GlobalFeature.ExternalDocumentation, GlobalFeature.Examples,
                        GlobalFeature.Callbacks
                        // TODO: xml structures, styles, link objects, parameterized servers, full schemes for OAS 2.0
                )
                .includeSchemaSupportFeatures(
                        SchemaSupportFeature.Simple, SchemaSupportFeature.Composite,
                        SchemaSupportFeature.Polymorphism
                        // Union (OneOf) not 100% yet.
                )
                .includeParameterFeatures(
                        ParameterFeature.Path, ParameterFeature.Query, ParameterFeature.Header, ParameterFeature.Body,
                        ParameterFeature.FormUnencoded, ParameterFeature.FormMultipart, ParameterFeature.Cookie
                )
                .includeSecurityFeatures(
                        SecurityFeature.BasicAuth, SecurityFeature.ApiKey, SecurityFeature.BearerToken,
                        SecurityFeature.OAuth2_Implicit, SecurityFeature.OAuth2_Password,
                        SecurityFeature.OAuth2_ClientCredentials, SecurityFeature.OAuth2_AuthorizationCode
                        // OpenIdConnect and SignatureAuth and AW4Signature are not yet 100% supported
                )
                .includeWireFormatFeatures(
                        WireFormatFeature.JSON, WireFormatFeature.XML
                        // PROTOBUF and Custom are generator specific
                )
                .build();

        int cacheSize = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_SIZE_PROPERTY, "500"));
        int cacheExpiry = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_EXPIRY_PROPERTY, "10"));
        sanitizedNameCache = Caffeine.newBuilder()
                .maximumSize(cacheSize)
                .expireAfterAccess(cacheExpiry, TimeUnit.SECONDS)
                .ticker(Ticker.systemTicker())
                .build();
        falseSchema = new Schema();
        falseSchema.setNot(new Schema());
    }

    protected GeneratorMetadata generatorMetadata;
    protected String inputSpec;
    protected String outputFolder = "";
    protected Set defaultIncludes;
    protected Map typeMapping;
    // instantiationTypes map from container types only: set, map, and array to the in language-type
    protected Map instantiationTypes;
    protected Set reservedWords;
    protected Set languageSpecificPrimitives = new HashSet<>();
    protected Set openapiGeneratorIgnoreList = new HashSet<>();
    protected Map importMapping = new HashMap<>();
    // a map to store the mapping between a schema and the new one
    protected Map schemaMapping = new HashMap<>();
    // a map to store the mapping between inline schema and the name provided by the user
    protected Map inlineSchemaNameMapping = new HashMap<>();
    // a map to store the inline schema naming conventions
    protected Map inlineSchemaOption = new HashMap<>();
    // a map to store the mapping between property name and the name provided by the user
    protected Map nameMapping = new HashMap<>();
    // a map to store the mapping between parameter name and the name provided by the user
    protected Map parameterNameMapping = new HashMap<>();
    // a map to store the mapping between model name and the name provided by the user
    protected Map modelNameMapping = new HashMap<>();
    // a map to store the mapping between enum name and the name provided by the user
    protected Map enumNameMapping = new HashMap<>();
    // a map to store the rules in OpenAPI Normalizer
    protected Map openapiNormalizer = new HashMap<>();
    protected String modelPackage = "", apiPackage = "", fileSuffix;
    protected String modelNamePrefix = "", modelNameSuffix = "";
    protected String apiNamePrefix = "", apiNameSuffix = "Api";
    protected String testPackage = "";
    protected String filesMetadataFilename = "FILES";
    protected String versionMetadataFilename = "VERSION";
    /*
    apiTemplateFiles are for API outputs only (controllers/handlers).
    API templates may be written multiple times; APIs are grouped by tag and the file is written once per tag group.
    */
    protected Map apiTemplateFiles = new HashMap<>();
    protected Map modelTemplateFiles = new HashMap<>();
    protected Map apiTestTemplateFiles = new HashMap<>();
    protected Map modelTestTemplateFiles = new HashMap<>();
    protected Map apiDocTemplateFiles = new HashMap<>();
    protected Map modelDocTemplateFiles = new HashMap<>();
    protected Map reservedWordsMappings = new HashMap<>();
    protected String templateDir;
    protected String embeddedTemplateDir;
    protected Map additionalProperties = new HashMap<>();
    protected Map serverVariables = new HashMap<>();
    protected Map vendorExtensions = new HashMap<>();
    protected Map templateOutputDirs = new HashMap<>();
    /*
    Supporting files are those which aren't models, APIs, or docs.
    These get a different map of data bound to the templates. Supporting files are written once.
    See also 'apiTemplateFiles'.
    */
    protected List supportingFiles = new ArrayList<>();
    protected List cliOptions = new ArrayList<>();
    protected boolean skipOverwrite;
    protected boolean removeOperationIdPrefix;
    protected String removeOperationIdPrefixDelimiter = "_";
    protected int removeOperationIdPrefixCount = 1;
    protected boolean skipOperationExample;

    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");

    /**
     * True if the code generator supports multiple class inheritance.
     * This is used to model the parent hierarchy based on the 'allOf' composed schemas.
     */
    protected boolean supportsMultipleInheritance;
    /**
     * True if the code generator supports single class inheritance.
     * This is used to model the parent hierarchy based on the 'allOf' composed schemas.
     * Note: the single-class inheritance technique has inherent limitations because
     * a 'allOf' composed schema may have multiple $ref child schemas, each one
     * potentially representing a "parent" in the class inheritance hierarchy.
     * Some language generators also use class inheritance to implement the `additionalProperties`
     * keyword. For example, the Java code generator may generate 'extends HashMap'.
     */
    protected boolean supportsInheritance;
    /**
     * True if the language generator supports the 'additionalProperties' keyword
     * as sibling of a composed (allOf/anyOf/oneOf) schema.
     * Note: all language generators should support this to comply with the OAS specification.
     */
    protected boolean supportsAdditionalPropertiesWithComposedSchema;
    protected boolean supportsMixins;
    protected Map supportedLibraries = new LinkedHashMap<>();
    protected String library;
    protected Boolean sortParamsByRequiredFlag = true;
    protected Boolean sortModelPropertiesByRequiredFlag = false;
    protected Boolean ensureUniqueParams = true;
    protected Boolean allowUnicodeIdentifiers = false;
    protected String gitHost, gitUserId, gitRepoId, releaseNote;
    protected String httpUserAgent;
    protected Boolean hideGenerationTimestamp = true;
    // How to encode special characters like $
    // They are translated to words like "Dollar"
    // Then translated back during JSON encoding and decoding
    protected Map specialCharReplacements = new LinkedHashMap<>();
    // When a model is an alias for a simple type
    protected Map typeAliases = Collections.emptyMap();
    protected Boolean prependFormOrBodyParameters = false;
    // The extension of the generated documentation files (defaults to markdown .md)
    protected String docExtension;
    protected String ignoreFilePathOverride;
    // flag to indicate whether to use environment variable to post process file
    protected boolean enablePostProcessFile = false;
    private TemplatingEngineAdapter templatingEngine = new MustacheEngineAdapter();
    // flag to indicate whether to use the utils.OneOfImplementorAdditionalData related logic
    protected boolean useOneOfInterfaces = false;
    // whether or not the oneOf imports machinery should add oneOf interfaces as imports in implementing classes
    protected boolean addOneOfInterfaceImports = false;
    protected List addOneOfInterfaces = new ArrayList<>();

    // flag to indicate whether to only update files whose contents have changed
    protected boolean enableMinimalUpdate = false;

    // acts strictly upon a spec, potentially modifying it to have consistent behavior across generators.
    protected boolean strictSpecBehavior = true;
    // flag to indicate whether enum value prefixes are removed
    protected boolean removeEnumValuePrefix = true;

    // Support legacy logic for evaluating discriminators
    protected boolean legacyDiscriminatorBehavior = true;

    // Specify what to do if the 'additionalProperties' keyword is not present in a schema.
    // See CodegenConstants.java for more details.
    protected boolean disallowAdditionalPropertiesIfNotPresent = true;

    // If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.
    // With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.
    protected boolean enumUnknownDefaultCase = false;
    protected String enumUnknownDefaultCaseName = "unknown_default_open_api";

    // make openapi available to all methods
    protected OpenAPI openAPI;

    // A cache to efficiently lookup a Schema instance based on the return value of `toModelName()`.
    private Map modelNameToSchemaCache;

    // A cache to efficiently lookup schema `toModelName()` based on the schema Key
    private final Map schemaKeyToModelNameCache = new HashMap<>();

    protected boolean loadDeepObjectIntoItems = true;

    // if true then baseTypes will be imported
    protected boolean importBaseType = true;

    // if true then container types will be imported
    protected boolean importContainerType = true;

    protected boolean addSuffixToDuplicateOperationNicknames = true;

    // Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
    protected boolean autosetConstants = false;

    public boolean getAddSuffixToDuplicateOperationNicknames() {
        return addSuffixToDuplicateOperationNicknames;
    }

    @Override
    public List cliOptions() {
        return cliOptions;
    }

    @Override
    public void processOpts() {

        if (additionalProperties.containsKey(CodegenConstants.TEMPLATE_DIR)) {
            this.setTemplateDir((String) additionalProperties.get(CodegenConstants.TEMPLATE_DIR));
        }

        if (additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) {
            this.setModelPackage((String) additionalProperties.get(CodegenConstants.MODEL_PACKAGE));
        }

        if (additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) {
            this.setApiPackage((String) additionalProperties.get(CodegenConstants.API_PACKAGE));
        }

        if (additionalProperties.containsKey(CodegenConstants.HIDE_GENERATION_TIMESTAMP)) {
            setHideGenerationTimestamp(convertPropertyToBooleanAndWriteBack(CodegenConstants.HIDE_GENERATION_TIMESTAMP));
        } else {
            additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, hideGenerationTimestamp);
        }

        if (additionalProperties.containsKey(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG)) {
            this.setSortParamsByRequiredFlag(Boolean.valueOf(additionalProperties
                    .get(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG)) {
            this.setSortModelPropertiesByRequiredFlag(Boolean.valueOf(additionalProperties
                    .get(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS)) {
            this.setPrependFormOrBodyParameters(Boolean.valueOf(additionalProperties
                    .get(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.ENSURE_UNIQUE_PARAMS)) {
            this.setEnsureUniqueParams(Boolean.valueOf(additionalProperties
                    .get(CodegenConstants.ENSURE_UNIQUE_PARAMS).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS)) {
            this.setAllowUnicodeIdentifiers(Boolean.valueOf(additionalProperties
                    .get(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.API_NAME_PREFIX)) {
            this.setApiNamePrefix((String) additionalProperties.get(CodegenConstants.API_NAME_PREFIX));
        }

        if (additionalProperties.containsKey(CodegenConstants.API_NAME_SUFFIX)) {
            this.setApiNameSuffix((String) additionalProperties.get(CodegenConstants.API_NAME_SUFFIX));
        }

        if (additionalProperties.containsKey(CodegenConstants.MODEL_NAME_PREFIX)) {
            this.setModelNamePrefix((String) additionalProperties.get(CodegenConstants.MODEL_NAME_PREFIX));
        }

        if (additionalProperties.containsKey(CodegenConstants.MODEL_NAME_SUFFIX)) {
            this.setModelNameSuffix((String) additionalProperties.get(CodegenConstants.MODEL_NAME_SUFFIX));
        }

        if (additionalProperties.containsKey(CodegenConstants.REMOVE_OPERATION_ID_PREFIX)) {
            this.setRemoveOperationIdPrefix(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.REMOVE_OPERATION_ID_PREFIX).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.REMOVE_OPERATION_ID_PREFIX_DELIMITER)) {
            this.setRemoveOperationIdPrefixDelimiter(additionalProperties
                    .get(CodegenConstants.REMOVE_OPERATION_ID_PREFIX_DELIMITER).toString());
        }

        if (additionalProperties.containsKey(CodegenConstants.REMOVE_OPERATION_ID_PREFIX_COUNT)) {
            this.setRemoveOperationIdPrefixCount(Integer.parseInt(additionalProperties
                    .get(CodegenConstants.REMOVE_OPERATION_ID_PREFIX_COUNT).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.SKIP_OPERATION_EXAMPLE)) {
            this.setSkipOperationExample(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.SKIP_OPERATION_EXAMPLE).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.DOCEXTENSION)) {
            this.setDocExtension(String.valueOf(additionalProperties
                    .get(CodegenConstants.DOCEXTENSION).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.ENABLE_POST_PROCESS_FILE)) {
            this.setEnablePostProcessFile(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.ENABLE_POST_PROCESS_FILE).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.GENERATE_ALIAS_AS_MODEL)) {
            ModelUtils.setGenerateAliasAsModel(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.GENERATE_ALIAS_AS_MODEL).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.REMOVE_ENUM_VALUE_PREFIX)) {
            this.setRemoveEnumValuePrefix(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.REMOVE_ENUM_VALUE_PREFIX).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR)) {
            this.setLegacyDiscriminatorBehavior(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR).toString()));
        }
        if (additionalProperties.containsKey(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT)) {
            this.setDisallowAdditionalPropertiesIfNotPresent(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT).toString()));
        }
        if (additionalProperties.containsKey(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE)) {
            this.setEnumUnknownDefaultCase(Boolean.parseBoolean(additionalProperties
                    .get(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE).toString()));
        }
        if (additionalProperties.containsKey(CodegenConstants.AUTOSET_CONSTANTS)) {
            this.setAutosetConstants(
                    Boolean.parseBoolean(additionalProperties.get(CodegenConstants.AUTOSET_CONSTANTS).toString()));
        }
    }

    /***
     * Preset map builder with commonly used Mustache lambdas.
     *
     * To extend the map, override addMustacheLambdas(), call parent method
     * first and then add additional lambdas to the returned builder.
     *
     * If common lambdas are not desired, override addMustacheLambdas() method
     * and return empty builder.
     *
     * @return preinitialized map with common lambdas
     */
    protected ImmutableMap.Builder addMustacheLambdas() {

        return new ImmutableMap.Builder()
                .put("lowercase", new LowercaseLambda().generator(this))
                .put("uppercase", new UppercaseLambda())
                .put("snakecase", new SnakecaseLambda())
                .put("titlecase", new TitlecaseLambda())
                .put("camelcase", new CamelCaseLambda(true).generator(this))
                .put("pascalcase", new CamelCaseLambda(false).generator(this))
                .put("forwardslash", new ForwardSlashLambda())
                .put("backslash", new BackSlashLambda())
                .put("doublequote", new DoubleQuoteLambda())
                .put("indented", new IndentedLambda())
                .put("indented_8", new IndentedLambda(8, " ", false, false))
                .put("indented_12", new IndentedLambda(12, " ", false, false))
                .put("indented_16", new IndentedLambda(16, " ", false, false));

    }

    private void registerMustacheLambdas() {
        ImmutableMap lambdas = addMustacheLambdas().build();

        if (lambdas.size() == 0) {
            return;
        }

        if (additionalProperties.containsKey("lambda")) {
            LOGGER.error("A property called 'lambda' already exists in additionalProperties");
            throw new RuntimeException("A property called 'lambda' already exists in additionalProperties");
        }
        additionalProperties.put("lambda", lambdas);
    }

    // override with any special post-processing for all models
    @Override
    @SuppressWarnings("static-method")
    public Map postProcessAllModels(Map objs) {
        for (Map.Entry entry : objs.entrySet()) {
            CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs);

            if (model == null) {
                LOGGER.warn("Null model found in postProcessAllModels: {}", entry.getKey());
                continue;
            }

            // add the model to the discriminator's mapping so templates have access to more than just the string to string mapping
            if (model.discriminator != null && model.discriminator.getMappedModels() != null) {
                for (CodegenDiscriminator.MappedModel mappedModel : model.discriminator.getMappedModels()) {
                    CodegenModel mappedCodegenModel = ModelUtils.getModelByName(mappedModel.getModelName(), objs);
                    mappedModel.setModel(mappedCodegenModel);
                }
            }

            for (CodegenProperty property : model.allVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.vars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.readWriteVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.optionalVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.parentVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.requiredVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.readOnlyVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
            for (CodegenProperty property : model.nonNullableVars) {
                property.isNew = codegenPropertyIsNew(model, property);
            }
        }

        if (this.useOneOfInterfaces) {
            // First, add newly created oneOf interfaces
            for (CodegenModel cm : addOneOfInterfaces) {
                ModelMap modelMapValue = new ModelMap(additionalProperties());
                modelMapValue.setModel(cm);

                List> importsValue = new ArrayList<>();
                ModelsMap objsValue = new ModelsMap();
                objsValue.setModels(Collections.singletonList(modelMapValue));
                objsValue.put("package", modelPackage());
                objsValue.setImports(importsValue);
                objsValue.put("classname", cm.classname);
                objsValue.putAll(additionalProperties);
                objs.put(cm.name, objsValue);
            }

            // Gather data from all the models that contain oneOf into OneOfImplementorAdditionalData classes
            // (see docstring of that class to find out what information is gathered and why)
            Map additionalDataMap = new HashMap<>();
            for (ModelsMap modelsAttrs : objs.values()) {
                List> modelsImports = modelsAttrs.getImportsOrEmpty();
                for (ModelMap mo : modelsAttrs.getModels()) {
                    CodegenModel cm = mo.getModel();
                    if (cm.oneOf.size() > 0) {
                        cm.vendorExtensions.put("x-is-one-of-interface", true);
                        for (String one : cm.oneOf) {
                            if (!additionalDataMap.containsKey(one)) {
                                additionalDataMap.put(one, new OneOfImplementorAdditionalData(one));
                            }
                            additionalDataMap.get(one).addFromInterfaceModel(cm, modelsImports);
                        }
                        // if this is oneOf interface, make sure we include the necessary imports for it
                        addImportsToOneOfInterface(modelsImports);
                    }
                }
            }

            // Add all the data from OneOfImplementorAdditionalData classes to the implementing models
            for (Map.Entry modelsEntry : objs.entrySet()) {
                ModelsMap modelsAttrs = modelsEntry.getValue();
                List> imports = modelsAttrs.getImports();
                for (ModelMap implmo : modelsAttrs.getModels()) {
                    CodegenModel implcm = implmo.getModel();
                    String modelName = toModelName(implcm.name);
                    if (additionalDataMap.containsKey(modelName)) {
                        additionalDataMap.get(modelName).addToImplementor(this, implcm, imports, addOneOfInterfaceImports);
                    }
                }
            }
        }

        return objs;
    }

    private boolean codegenPropertyIsNew(CodegenModel model, CodegenProperty property) {
        return model.parentModel == null
                ? false
                : model.parentModel.allVars.stream().anyMatch(p -> p.name.equals(property.name) && (p.dataType.equals(property.dataType) == false || p.datatypeWithEnum.equals(property.datatypeWithEnum) == false));
    }

    /**
     * Return a map from model name to Schema for efficient lookup.
     *
     * @return map from model name to Schema.
     */
    protected Map getModelNameToSchemaCache() {
        if (modelNameToSchemaCache == null) {
            // Create a cache to efficiently lookup schema based on model name.
            Map m = new HashMap<>();
            ModelUtils.getSchemas(openAPI).forEach((key, schema) -> m.put(toModelName(key), schema));
            modelNameToSchemaCache = Collections.unmodifiableMap(m);
        }
        return modelNameToSchemaCache;
    }

    /**
     * Index all CodegenModels by model name.
     *
     * @param objs Map of models
     * @return map of all models indexed by names
     */
    public Map getAllModels(Map objs) {
        Map allModels = new LinkedHashMap<>();
        for (Entry entry : objs.entrySet()) {
            String modelName = toModelName(entry.getKey());
            List models = entry.getValue().getModels();
            for (ModelMap mo : models) {
                CodegenModel cm = mo.getModel();
                allModels.put(modelName, cm);
            }
        }
        return allModels;
    }

    /**
     * Loop through all models to update different flags (e.g. isSelfReference), children models, etc
     * and update mapped models for import.
     *
     * @param objs Map of models
     * @return maps of models with various updates
     */
    @Override
    public Map updateAllModels(Map objs) {
        Map allModels = getAllModels(objs);

        // Fix up all parent and interface CodegenModel references.
        for (CodegenModel cm : allModels.values()) {
            if (cm.getParent() != null) {
                cm.setParentModel(allModels.get(cm.getParent()));
            }
            if (cm.getInterfaces() != null && !cm.getInterfaces().isEmpty()) {
                cm.setInterfaceModels(new ArrayList<>(cm.getInterfaces().size()));
                for (String intf : cm.getInterfaces()) {
                    CodegenModel intfModel = allModels.get(intf);
                    if (intfModel != null) {
                        cm.getInterfaceModels().add(intfModel);
                    }
                }
            }
        }

        // Let parent know about all its children
        for (Map.Entry allModelsEntry : allModels.entrySet()) {
            String name = allModelsEntry.getKey();
            CodegenModel cm = allModelsEntry.getValue();
            CodegenModel parent = allModels.get(cm.getParent());
            // if a discriminator exists on the parent, don't add this child to the inheritance hierarchy
            // TODO Determine what to do if the parent discriminator name == the grandparent discriminator name
            while (parent != null) {
                if (parent.getChildren() == null) {
                    parent.setChildren(new ArrayList<>());
                }
                parent.getChildren().add(cm);
                parent.hasChildren = true;
                Schema parentSchema = this.openAPI.getComponents().getSchemas().get(parent.name);
                if (parentSchema == null) {
                    throw new NullPointerException(parent.name + " in " + this.openAPI.getComponents().getSchemas());
                }
                if (parentSchema.getDiscriminator() == null) {
                    parent = allModels.get(parent.getParent());
                } else {
                    parent = null;
                }
            }
        }

        // loop through properties of each model to detect self-reference
        // and update mapped models for import
        for (ModelsMap entry : objs.values()) {
            for (ModelMap mo : entry.getModels()) {
                CodegenModel cm = mo.getModel();
                removeSelfReferenceImports(cm);

                if (!this.getLegacyDiscriminatorBehavior()) {
                    cm.addDiscriminatorMappedModelsImports(true);
                }
            }
        }
        setCircularReferences(allModels);

        return objs;
    }

    /**
     * Removes importToRemove from the imports of objs, if present.
     * This is useful to remove imports that are already present in operations-related template files, to avoid importing the same thing twice.
     *
     * @param objs imports will be removed from this objs' imports collection
     * @param importToRemove the import statement to be removed
     */
    protected void removeImport(OperationsMap objs, String importToRemove) {
        List> imports = objs.getImports();
        for (Iterator> itr = imports.iterator(); itr.hasNext(); ) {
            String itrImport = itr.next().get("import");
            if (itrImport.equals(importToRemove)) {
                itr.remove();
            }
        }
    }

    /**
     * Removes imports from the model that points to itself
     * Marks a self referencing property, if detected
     *
     * @param model Self imports will be removed from this model.imports collection
     */
    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))) {
                model.imports.remove(model.classname); // remove self import
                cp.isSelfReference = true;
            }
        }
    }

    public void setCircularReferences(Map models) {
        // for allVars
        final Map> allVarsDependencyMap = models.entrySet().stream()
                .collect(Collectors.toMap(Entry::getKey, entry -> getModelDependencies(entry.getValue().getAllVars())));

        models.keySet().forEach(name -> setCircularReferencesOnProperties(name, allVarsDependencyMap));

        // for vars
        final Map> varsDependencyMap = models.entrySet().stream()
                .collect(Collectors.toMap(Entry::getKey, entry -> getModelDependencies(entry.getValue().getVars())));

        models.keySet().forEach(name -> setCircularReferencesOnProperties(name, varsDependencyMap));

        // for oneOf
        final Map> oneOfDependencyMap = models.entrySet().stream()
                .collect(Collectors.toMap(Entry::getKey, entry -> getModelDependencies(
                        (entry.getValue().getComposedSchemas() != null && entry.getValue().getComposedSchemas().getOneOf() != null)
                                ? entry.getValue().getComposedSchemas().getOneOf() : new ArrayList())));

        models.keySet().forEach(name -> setCircularReferencesOnProperties(name, oneOfDependencyMap));
    }

    private List getModelDependencies(List vars) {
        return vars.stream()
                .map(prop -> {
                    if (prop.isContainer) {
                        return prop.items.dataType == null ? null : prop;
                    }
                    return prop.dataType == null ? null : prop;
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private void setCircularReferencesOnProperties(final String root,
                                                   final Map> dependencyMap) {
        dependencyMap.getOrDefault(root, new ArrayList<>())
                .forEach(prop -> {
                    final List unvisited =
                            Collections.singletonList(prop.isContainer ? prop.items.dataType : prop.dataType);
                    prop.isCircularReference = isCircularReference(root,
                            new HashSet<>(),
                            new ArrayList<>(unvisited),
                            dependencyMap);
                });
    }

    private boolean isCircularReference(final String root,
                                        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)) {
                if (next.equals(root)) {
                    return true;
                }
                dependencyMap.getOrDefault(next, new ArrayList<>())
                        .forEach(prop -> unvisited.add(prop.isContainer ? prop.items.dataType : prop.dataType));
                visited.add(next);
            }
        }
        return false;
    }

    // override with any special post-processing
    @Override
    @SuppressWarnings("static-method")
    public ModelsMap postProcessModels(ModelsMap objs) {
        return objs;
    }

    /**
     * post process enum defined in model's properties
     *
     * @param objs Map of models
     * @return maps of models with better enum support
     */
    public ModelsMap postProcessModelsEnum(ModelsMap objs) {
        for (ModelMap mo : objs.getModels()) {
            CodegenModel cm = mo.getModel();

            // for enum model
            if (Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null) {
                Map allowableValues = cm.allowableValues;
                List values = (List) allowableValues.get("values");
                List> enumVars = buildEnumVars(values, cm.dataType);
                postProcessEnumVars(enumVars);
                // if "x-enum-varnames" or "x-enum-descriptions" defined, update varnames
                updateEnumVarsWithExtensions(enumVars, cm.getVendorExtensions(), cm.dataType);
                cm.allowableValues.put("enumVars", enumVars);
            }

            // update codegen property enum with proper naming convention
            // and handling of numbers, special characters
            for (CodegenProperty var : cm.vars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.allVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.nonNullableVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.requiredVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.optionalVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.parentVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.readOnlyVars) {
                updateCodegenPropertyEnum(var);
            }

            for (CodegenProperty var : cm.readWriteVars) {
                updateCodegenPropertyEnum(var);
            }

        }
        return objs;
    }

    /**
     * Returns the common prefix of variables for enum naming if
     * two or more variables are present
     *
     * @param vars List of variable names
     * @return the common prefix for naming
     */
    public String findCommonPrefixOfVars(List vars) {
        if (vars.size() > 1) {
            try {
                String[] listStr = vars.toArray(new String[vars.size()]);
                String prefix = StringUtils.getCommonPrefix(listStr);
                // exclude trailing characters that should be part of a valid variable
                // e.g. ["status-on", "status-off"] => "status-" (not "status-o")
                final Matcher matcher = COMMON_PREFIX_ENUM_NAME.matcher(prefix);
                return matcher.replaceAll("");
            } catch (ArrayStoreException e) {
                // do nothing, just return default value
            }
        }
        return "";
    }

    /**
     * Return the enum default value in the language specified format
     *
     * @param value    enum variable name
     * @param datatype data type
     * @return the default value for the enum
     */
    public String toEnumDefaultValue(String value, String datatype) {
        return datatype + "." + value;
    }

    /**
     * Return the enum value in the language specified format
     * e.g. status becomes "status"
     *
     * @param value    enum variable name
     * @param datatype data type
     * @return the sanitized value for enum
     */
    public String toEnumValue(String value, String datatype) {
        if ("number".equalsIgnoreCase(datatype) || "boolean".equalsIgnoreCase(datatype)) {
            return value;
        } else {
            return "\"" + escapeText(value) + "\"";
        }
    }

    /**
     * Return the sanitized variable name for enum
     *
     * @param value    enum variable name
     * @param datatype data type
     * @return the sanitized variable name for enum
     */
    public String toEnumVarName(String value, String datatype) {
        if (value.length() == 0) {
            return "EMPTY";
        }

        String var = value.replaceAll("\\W+", "_").toUpperCase(Locale.ROOT);
        if (var.matches("\\d.*")) {
            return "_" + var;
        } else {
            return var;
        }
    }

    public boolean specVersionGreaterThanOrEqualTo310(OpenAPI openAPI) {
        String originalSpecVersion;
        String xOriginalSwaggerVersion = "x-original-swagger-version";
        if (openAPI.getExtensions() != null && !openAPI.getExtensions().isEmpty() && openAPI.getExtensions().containsValue(xOriginalSwaggerVersion)) {
            originalSpecVersion = (String) openAPI.getExtensions().get(xOriginalSwaggerVersion);
        } else {
            originalSpecVersion = openAPI.getOpenapi();
        }
        Integer specMajorVersion = Integer.parseInt(originalSpecVersion.substring(0, 1));
        Integer specMinorVersion = Integer.parseInt(originalSpecVersion.substring(2, 3));
        return specMajorVersion == 3 && specMinorVersion >= 1;
    }

    /**
     * Set the OpenAPI document.
     * This method is invoked when the input OpenAPI document has been parsed and validated.
     */
    @Override
    public void setOpenAPI(OpenAPI openAPI) {
        if (specVersionGreaterThanOrEqualTo310(openAPI)) {
            LOGGER.warn(UNSUPPORTED_V310_SPEC_MSG);
        }
        this.openAPI = openAPI;
        // Set global settings such that helper functions in ModelUtils can lookup the value
        // of the CLI option.
        ModelUtils.setDisallowAdditionalPropertiesIfNotPresent(getDisallowAdditionalPropertiesIfNotPresent());

        // Multiple operations rely on proper type aliases, so we should always update them
        typeAliases = getAllAliases(ModelUtils.getSchemas(openAPI));
    }

    // override with any message to be shown right before the process finishes
    @Override
    @SuppressWarnings("static-method")
    public void postProcess() {
        System.out.println("################################################################################");
        System.out.println("# Thanks for using OpenAPI Generator.                                          #");
        System.out.println("# Please consider donation to help us maintain this project \uD83D\uDE4F                 #");
        System.out.println("# https://opencollective.com/openapi_generator/donate                          #");
        System.out.println("################################################################################");
    }

    // override with any special post-processing
    @Override
    @SuppressWarnings("static-method")
    public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
        return objs;
    }

    // override with any special post-processing
    @Override
    @SuppressWarnings("static-method")
    public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
        return objs;
    }

    // override with any special post-processing
    @Override
    @SuppressWarnings("static-method")
    public Map postProcessSupportingFileData(Map objs) {
        return objs;
    }

    // override to post-process any model properties
    @Override
    @SuppressWarnings("unused")
    public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
    }

    // override to post-process any response
    @Override
    @SuppressWarnings("unused")
    public void postProcessResponseWithProperty(CodegenResponse response, CodegenProperty property) {
    }

    // override to post-process any parameters
    @Override
    @SuppressWarnings("unused")
    public void postProcessParameter(CodegenParameter parameter) {
    }

    //override with any special handling of the entire OpenAPI spec document
    @Override
    @SuppressWarnings("unused")
    public void preprocessOpenAPI(OpenAPI openAPI) {
        if (useOneOfInterfaces && openAPI.getComponents() != null) {
            // we process the openapi schema here to find oneOf schemas and create interface models for them
            Map schemas = new HashMap<>(openAPI.getComponents().getSchemas());
            if (schemas == null) {
                schemas = new HashMap<>();
            }
            Map pathItems = openAPI.getPaths();

            // we need to add all request and response bodies to processed schemas
            if (pathItems != null) {
                for (Map.Entry e : pathItems.entrySet()) {
                    for (Map.Entry op : e.getValue().readOperationsMap().entrySet()) {
                        String opId = getOrGenerateOperationId(op.getValue(), e.getKey(), op.getKey().toString());
                        // process request body
                        RequestBody b = ModelUtils.getReferencedRequestBody(openAPI, op.getValue().getRequestBody());
                        Schema requestSchema = null;
                        if (b != null) {
                            requestSchema = ModelUtils.getSchemaFromRequestBody(b);
                        }
                        if (requestSchema != null) {
                            schemas.put(opId, requestSchema);
                        }
                        // process all response bodies
                        if (op.getValue().getResponses() != null) {
                            for (Map.Entry ar : op.getValue().getResponses().entrySet()) {
                                ApiResponse a = ModelUtils.getReferencedApiResponse(openAPI, ar.getValue());
                                Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, a));
                                if (responseSchema != null) {
                                    schemas.put(opId + ar.getKey(), responseSchema);
                                }
                            }
                        }
                    }
                }
            }

            // also add all properties of all schemas to be checked for oneOf
            Map propertySchemas = new HashMap<>();
            for (Map.Entry e : schemas.entrySet()) {
                Schema s = e.getValue();
                Map props = s.getProperties();
                if (props == null) {
                    props = new HashMap<>();
                }
                for (Map.Entry p : props.entrySet()) {
                    propertySchemas.put(e.getKey() + "/" + p.getKey(), p.getValue());
                }
            }
            schemas.putAll(propertySchemas);

            // go through all gathered schemas and add them as interfaces to be created
            for (Map.Entry e : schemas.entrySet()) {
                String n = toModelName(e.getKey());
                Schema s = e.getValue();
                String nOneOf = toModelName(n + "OneOf");
                if (ModelUtils.isComposedSchema(s)) {
                    if (e.getKey().contains("/")) {
                        // if this is property schema, we also need to generate the oneOf interface model
                        addOneOfNameExtension(s, nOneOf);
                        addOneOfInterfaceModel(s, nOneOf);
                    } else {
                        // else this is a component schema, so we will just use that as the oneOf interface model
                        addOneOfNameExtension(s, n);
                    }
                } else if (ModelUtils.isArraySchema(s)) {
                    Schema items = ((ArraySchema) s).getItems();
                    if (ModelUtils.isComposedSchema(items)) {
                        addOneOfNameExtension(items, nOneOf);
                        addOneOfInterfaceModel(items, nOneOf);
                    }
                } else if (ModelUtils.isMapSchema(s)) {
                    Schema addProps = ModelUtils.getAdditionalProperties(s);
                    if (addProps != null && ModelUtils.isComposedSchema(addProps)) {
                        addOneOfNameExtension(addProps, nOneOf);
                        addOneOfInterfaceModel(addProps, nOneOf);
                    }
                }
            }
        }
    }

    // override with any special handling of the entire OpenAPI spec document
    @Override
    @SuppressWarnings("unused")
    public void processOpenAPI(OpenAPI openAPI) {
    }

    // override with any special handling of the JMustache compiler
    @Override
    @SuppressWarnings("unused")
    public Compiler processCompiler(Compiler compiler) {
        return compiler;
    }

    // override with any special handling for the templating engine
    @Override
    @SuppressWarnings("unused")
    public TemplatingEngineAdapter processTemplatingEngine(TemplatingEngineAdapter templatingEngine) {
        return templatingEngine;
    }

    // override with any special text escaping logic
    @Override
    @SuppressWarnings("static-method")
    public String escapeText(String input) {
        if (input == null) {
            return input;
        }

        // remove \t, \n, \r
        // replace \ with \\
        // replace " with \"
        // outer unescape to retain the original multi-byte characters
        // finally escalate characters avoiding code injection
        return escapeUnsafeCharacters(
                StringEscapeUtils.unescapeJava(
                                StringEscapeUtils.escapeJava(input)
                                        .replace("\\/", "/"))
                        .replaceAll("[\\t\\n\\r]", " ")
                        .replace("\\", "\\\\")
                        .replace("\"", "\\\""));
    }

    /**
     * Escape characters while allowing new lines
     *
     * @param input String to be escaped
     * @return escaped string
     */
    @Override
    public String escapeTextWhileAllowingNewLines(String input) {
        if (input == null) {
            return input;
        }

        // remove \t
        // replace \ with \\
        // replace " with \"
        // outer unescape to retain the original multi-byte characters
        // finally escalate characters avoiding code injection
        return escapeUnsafeCharacters(
                StringEscapeUtils.unescapeJava(
                                StringEscapeUtils.escapeJava(input)
                                        .replace("\\/", "/"))
                        .replaceAll("[\\t]", " ")
                        .replace("\\", "\\\\")
                        .replace("\"", "\\\""));
    }

    // override with any special encoding and escaping logic
    @Override
    @SuppressWarnings("static-method")
    public String encodePath(String input) {
        return escapeText(input);
    }

    /**
     * override with any special text escaping logic to handle unsafe
     * characters so as to avoid code injection
     *
     * @param input String to be cleaned up
     * @return string with unsafe characters removed or escaped
     */
    @Override
    public String escapeUnsafeCharacters(String input) {
        LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
                "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
        // code generator implements this method
        return input;
    }

    /**
     * Escape single and/or double quote to avoid code injection
     *
     * @param input String to be cleaned up
     * @return string with quotation mark removed or escaped
     */
    @Override
    public String escapeQuotationMark(String input) {
        LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
                "single/double quote");
        return input.replace("\"", "\\\"");
    }

    @Override
    public Set defaultIncludes() {
        return defaultIncludes;
    }

    @Override
    public Map typeMapping() {
        return typeMapping;
    }

    @Override
    public Map instantiationTypes() {
        return instantiationTypes;
    }

    @Override
    public Set reservedWords() {
        return reservedWords;
    }

    @Override
    public Set languageSpecificPrimitives() {
        return languageSpecificPrimitives;
    }

    @Override
    public Set openapiGeneratorIgnoreList() {
        return openapiGeneratorIgnoreList;
    }

    @Override
    public Map importMapping() {
        return importMapping;
    }

    @Override
    public Map schemaMapping() {
        return schemaMapping;
    }

    @Override
    public Map inlineSchemaNameMapping() {
        return inlineSchemaNameMapping;
    }

    @Override
    public Map inlineSchemaOption() {
        return inlineSchemaOption;
    }

    @Override
    public Map nameMapping() {
        return nameMapping;
    }

    @Override
    public Map parameterNameMapping() {
        return parameterNameMapping;
    }

    @Override
    public Map modelNameMapping() {
        return modelNameMapping;
    }

    @Override
    public Map enumNameMapping() {
        return enumNameMapping;
    }

    @Override
    public Map openapiNormalizer() {
        return openapiNormalizer;
    }

    @Override
    public String testPackage() {
        return testPackage;
    }

    @Override
    public String modelPackage() {
        return modelPackage;
    }

    @Override
    public String apiPackage() {
        return apiPackage;
    }

    @Override
    public String fileSuffix() {
        return fileSuffix;
    }

    @Override
    public String templateDir() {
        return templateDir;
    }

    @Override
    public String embeddedTemplateDir() {
        if (embeddedTemplateDir != null) {
            return embeddedTemplateDir;
        } else {
            return templateDir;
        }
    }

    @Override
    public Map apiDocTemplateFiles() {
        return apiDocTemplateFiles;
    }

    @Override
    public Map modelDocTemplateFiles() {
        return modelDocTemplateFiles;
    }

    @Override
    public Map reservedWordsMappings() {
        return reservedWordsMappings;
    }

    @Override
    public Map apiTestTemplateFiles() {
        return apiTestTemplateFiles;
    }

    @Override
    public Map modelTestTemplateFiles() {
        return modelTestTemplateFiles;
    }

    @Override
    public Map apiTemplateFiles() {
        return apiTemplateFiles;
    }

    @Override
    public Map modelTemplateFiles() {
        return modelTemplateFiles;
    }

    @Override
    public String apiFileFolder() {
        return outputFolder + File.separator + apiPackage().replace('.', File.separatorChar);
    }

    @Override
    public String modelFileFolder() {
        return outputFolder + File.separator + modelPackage().replace('.', File.separatorChar);
    }

    @Override
    public String apiTestFileFolder() {
        return outputFolder + File.separator + testPackage().replace('.', File.separatorChar);
    }

    @Override
    public String modelTestFileFolder() {
        return outputFolder + File.separator + testPackage().replace('.', File.separatorChar);
    }

    @Override
    public String apiDocFileFolder() {
        return outputFolder;
    }

    @Override
    public String modelDocFileFolder() {
        return outputFolder;
    }

    @Override
    public Map additionalProperties() {
        return additionalProperties;
    }

    @Override
    public Map serverVariableOverrides() {
        return serverVariables;
    }

    @Override
    public Map vendorExtensions() {
        return vendorExtensions;
    }

    @Override
    public Map templateOutputDirs() {
        return templateOutputDirs;
    }

    @Override
    public List supportingFiles() {
        return supportingFiles;
    }

    @Override
    public String outputFolder() {
        return outputFolder;
    }

    @Override
    public void setOutputDir(String dir) {
        this.outputFolder = dir;
    }

    @Override
    public String getOutputDir() {
        return outputFolder();
    }

    @Override
    public String getInputSpec() {
        return inputSpec;
    }

    @Override
    public void setInputSpec(String inputSpec) {
        this.inputSpec = inputSpec;
    }

    @Override
    public String getFilesMetadataFilename() {
        return filesMetadataFilename;
    }

    public void setFilesMetadataFilename(String filesMetadataFilename) {
        this.filesMetadataFilename = filesMetadataFilename;
    }

    @Override
    public String getVersionMetadataFilename() {
        return versionMetadataFilename;
    }

    public void setVersionMetadataFilename(String versionMetadataFilename) {
        this.versionMetadataFilename = versionMetadataFilename;
    }

    public void setTemplateDir(String templateDir) {
        this.templateDir = templateDir;
    }

    public void setModelPackage(String modelPackage) {
        this.modelPackage = modelPackage;
    }

    public String getModelNamePrefix() {
        return modelNamePrefix;
    }

    public void setModelNamePrefix(String modelNamePrefix) {
        this.modelNamePrefix = modelNamePrefix;
    }

    public String getModelNameSuffix() {
        return modelNameSuffix;
    }

    public void setModelNameSuffix(String modelNameSuffix) {
        this.modelNameSuffix = modelNameSuffix;
    }

    public String getApiNameSuffix() {
        return apiNameSuffix;
    }

    public void setApiNameSuffix(String apiNameSuffix) {
        this.apiNameSuffix = apiNameSuffix;
    }

    public String getApiNamePrefix() {
        return apiNamePrefix;
    }

    public void setApiNamePrefix(String apiNamePrefix) {
        this.apiNamePrefix = apiNamePrefix;
    }

    public void setApiPackage(String apiPackage) {
        this.apiPackage = apiPackage;
    }

    public Boolean getSortParamsByRequiredFlag() {
        return sortParamsByRequiredFlag;
    }

    public void setSortParamsByRequiredFlag(Boolean sortParamsByRequiredFlag) {
        this.sortParamsByRequiredFlag = sortParamsByRequiredFlag;
    }

    public Boolean getSortModelPropertiesByRequiredFlag() {
        return sortModelPropertiesByRequiredFlag;
    }

    public void setSortModelPropertiesByRequiredFlag(Boolean sortModelPropertiesByRequiredFlag) {
        this.sortModelPropertiesByRequiredFlag = sortModelPropertiesByRequiredFlag;
    }

    public Boolean getPrependFormOrBodyParameters() {
        return prependFormOrBodyParameters;
    }

    public void setPrependFormOrBodyParameters(Boolean prependFormOrBodyParameters) {
        this.prependFormOrBodyParameters = prependFormOrBodyParameters;
    }

    public Boolean getEnsureUniqueParams() {
        return ensureUniqueParams;
    }

    public void setEnsureUniqueParams(Boolean ensureUniqueParams) {
        this.ensureUniqueParams = ensureUniqueParams;
    }

    public Boolean getLegacyDiscriminatorBehavior() {
        return legacyDiscriminatorBehavior;
    }

    public void setLegacyDiscriminatorBehavior(boolean val) {
        this.legacyDiscriminatorBehavior = val;
    }

    public Boolean getDisallowAdditionalPropertiesIfNotPresent() {
        return disallowAdditionalPropertiesIfNotPresent;
    }

    public void setDisallowAdditionalPropertiesIfNotPresent(boolean val) {
        this.disallowAdditionalPropertiesIfNotPresent = val;
    }

    public Boolean getEnumUnknownDefaultCase() {
        return enumUnknownDefaultCase;
    }

    public void setEnumUnknownDefaultCase(boolean val) {
        this.enumUnknownDefaultCase = val;
    }

    public Boolean getAllowUnicodeIdentifiers() {
        return allowUnicodeIdentifiers;
    }

    public void setAllowUnicodeIdentifiers(Boolean allowUnicodeIdentifiers) {
        this.allowUnicodeIdentifiers = allowUnicodeIdentifiers;
    }

    public Boolean getUseOneOfInterfaces() {
        return useOneOfInterfaces;
    }

    public void setUseOneOfInterfaces(Boolean useOneOfInterfaces) {
        this.useOneOfInterfaces = useOneOfInterfaces;
    }

    /**
     * Return the regular expression/JSON schema pattern (http://json-schema.org/latest/json-schema-validation.html#anchor33)
     *
     * @param pattern the pattern (regular expression)
     * @return properly-escaped pattern
     */
    public String toRegularExpression(String pattern) {
        return addRegularExpressionDelimiter(escapeText(pattern));
    }

    /**
     * Return the file name of the Api Test
     *
     * @param name the file name of the Api
     * @return the file name of the Api
     */
    @Override
    public String toApiFilename(String name) {
        return toApiName(name);
    }

    /**
     * Return the file name of the Api Documentation
     *
     * @param name the file name of the Api
     * @return the file name of the Api
     */
    @Override
    public String toApiDocFilename(String name) {
        return toApiName(name);
    }

    /**
     * Return the file name of the Api Test
     *
     * @param name the file name of the Api
     * @return the file name of the Api
     */
    @Override
    public String toApiTestFilename(String name) {
        return toApiName(name) + "Test";
    }

    /**
     * Return the variable name in the Api
     *
     * @param name the variable name of the Api
     * @return the snake-cased variable name
     */
    @Override
    public String toApiVarName(String name) {
        return lowerCamelCase(name);
    }

    /**
     * Return the capitalized file name of the model
     *
     * @param name the model name
     * @return the file name of the model
     */
    @Override
    public String toModelFilename(String name) {
        return camelize(name);
    }

    /**
     * Return the capitalized file name of the model test
     *
     * @param name the model name
     * @return the file name of the model
     */
    @Override
    public String toModelTestFilename(String name) {
        return camelize(name) + "Test";
    }

    /**
     * Return the capitalized file name of the model documentation
     *
     * @param name the model name
     * @return the file name of the model
     */
    @Override
    public String toModelDocFilename(String name) {
        return camelize(name);
    }

    /**
     * Returns metadata about the generator.
     *
     * @return A provided {@link GeneratorMetadata} instance
     */
    @Override
    public GeneratorMetadata getGeneratorMetadata() {
        return generatorMetadata;
    }

    /**
     * Return the operation ID (method name)
     *
     * @param operationId operation ID
     * @return the sanitized method name
     */
    @SuppressWarnings("static-method")
    public String toOperationId(String operationId) {
        // throw exception if method name is empty
        if (StringUtils.isEmpty(operationId)) {
            throw new RuntimeException("Empty method name (operationId) not allowed");
        }

        return operationId;
    }

    /**
     * Return the variable name by removing invalid characters and proper escaping if
     * it's a reserved word.
     *
     * @param name the variable name
     * @return the sanitized variable name
     */
    public String toVarName(final String name) {
        // obtain the name from nameMapping directly if provided
        if (nameMapping.containsKey(name)) {
            return nameMapping.get(name);
        }

        if (reservedWords.contains(name)) {
            return escapeReservedWord(name);
        } else if (name.chars().anyMatch(character -> specialCharReplacements.containsKey(String.valueOf((char) character)))) {
            return escape(name, specialCharReplacements, null, null);
        }
        return name;
    }

    /**
     * Return the parameter name by removing invalid characters and proper escaping if
     * it's a reserved word.
     *
     * @param name Codegen property object
     * @return the sanitized parameter name
     */
    @Override
    public String toParamName(String name) {
        // obtain the name from parameterNameMapping directly if provided
        if (parameterNameMapping.containsKey(name)) {
            return parameterNameMapping.get(name);
        }

        name = removeNonNameElementToCamelCase(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
        if (reservedWords.contains(name)) {
            return escapeReservedWord(name);
        } else if (name.chars().anyMatch(character -> specialCharReplacements.containsKey(String.valueOf((char) character)))) {
            return escape(name, specialCharReplacements, null, null);
        }
        return name;

    }

    /**
     * Return the parameter name of array of model
     *
     * @param name name of the array model
     * @return the sanitized parameter name
     */
    public String toArrayModelParamName(String name) {
        return toParamName(name);
    }

    /**
     * Return the Enum name (e.g. StatusEnum given 'status')
     *
     * @param property Codegen property
     * @return the Enum name
     */
    @SuppressWarnings("static-method")
    public String toEnumName(CodegenProperty property) {
        return StringUtils.capitalize(property.name) + "Enum";
    }

    /**
     * Return the escaped name of the reserved word
     *
     * @param name the name to be escaped
     * @return the escaped reserved word
     * 

* throws Runtime exception as reserved word is not allowed (default behavior) */ @Override @SuppressWarnings("static-method") public String escapeReservedWord(String name) { throw new RuntimeException("reserved word " + name + " not allowed"); } /** * Return the fully-qualified "Model" name for import * * @param name the name of the "Model" * @return the fully-qualified "Model" name for import */ @Override public String toModelImport(String name) { if ("".equals(modelPackage())) { return name; } else { return modelPackage() + "." + name; } } /** * Returns the same content as [[toModelImport]] with key the fully-qualified Model name and value the initial input. * In case of union types this method has a key for each separate model and import. * * @param name the name of the "Model" * @return Map of fully-qualified models. */ @Override public Map toModelImportMap(String name) { return Collections.singletonMap(this.toModelImport(name), name); } /** * Return the fully-qualified "Api" name for import * * @param name the name of the "Api" * @return the fully-qualified "Api" name for import */ @Override public String toApiImport(String name) { return apiPackage() + "." + name; } /** * Default constructor. * This method will map between OAS type and language-specified type, as well as mapping * between OAS type and the corresponding import statement for the language. This will * also add some language specified CLI options, if any. * returns string presentation of the example path (it's a constructor) */ public DefaultCodegen() { CodegenType codegenType = getTag(); if (codegenType == null) { codegenType = CodegenType.OTHER; } generatorMetadata = GeneratorMetadata.newBuilder() .stability(Stability.STABLE) .featureSet(DefaultFeatureSet) .generationMessage(String.format(Locale.ROOT, "OpenAPI Generator: %s (%s)", getName(), codegenType.toValue())) .build(); defaultIncludes = new HashSet<>( Arrays.asList("double", "int", "long", "short", "char", "float", "String", "boolean", "Boolean", "Double", "Void", "Integer", "Long", "Float") ); typeMapping = new HashMap<>(); typeMapping.put("array", "List"); typeMapping.put("set", "Set"); typeMapping.put("map", "Map"); typeMapping.put("boolean", "Boolean"); typeMapping.put("string", "String"); typeMapping.put("int", "Integer"); typeMapping.put("float", "Float"); typeMapping.put("double", "Double"); typeMapping.put("number", "BigDecimal"); typeMapping.put("decimal", "BigDecimal"); typeMapping.put("DateTime", "Date"); typeMapping.put("long", "Long"); typeMapping.put("short", "Short"); typeMapping.put("integer", "Integer"); typeMapping.put("UnsignedInteger", "Integer"); typeMapping.put("UnsignedLong", "Long"); typeMapping.put("char", "String"); typeMapping.put("object", "Object"); // FIXME: java specific type should be in Java Based Abstract Impl's typeMapping.put("ByteArray", "byte[]"); typeMapping.put("binary", "File"); typeMapping.put("file", "File"); typeMapping.put("UUID", "UUID"); typeMapping.put("URI", "URI"); typeMapping.put("AnyType", "oas_any_type_not_mapped"); instantiationTypes = new HashMap<>(); reservedWords = new HashSet<>(); cliOptions.add(CliOption.newBoolean(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG_DESC).defaultValue(Boolean.TRUE.toString())); cliOptions.add(CliOption.newBoolean(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG, CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG_DESC).defaultValue(Boolean.TRUE.toString())); cliOptions.add(CliOption.newBoolean(CodegenConstants.ENSURE_UNIQUE_PARAMS, CodegenConstants .ENSURE_UNIQUE_PARAMS_DESC).defaultValue(Boolean.TRUE.toString())); // name formatting options cliOptions.add(CliOption.newBoolean(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, CodegenConstants .ALLOW_UNICODE_IDENTIFIERS_DESC).defaultValue(Boolean.FALSE.toString())); // option to change the order of form/body parameter cliOptions.add(CliOption.newBoolean(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS_DESC).defaultValue(Boolean.FALSE.toString())); // option to change how we process + set the data in the discriminator mapping CliOption legacyDiscriminatorBehaviorOpt = CliOption.newBoolean(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR_DESC).defaultValue(Boolean.TRUE.toString()); Map legacyDiscriminatorBehaviorOpts = new HashMap<>(); legacyDiscriminatorBehaviorOpts.put("true", "The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document."); legacyDiscriminatorBehaviorOpts.put("false", "The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing."); legacyDiscriminatorBehaviorOpt.setEnum(legacyDiscriminatorBehaviorOpts); cliOptions.add(legacyDiscriminatorBehaviorOpt); // option to change how we process + set the data in the 'additionalProperties' keyword. CliOption disallowAdditionalPropertiesIfNotPresentOpt = CliOption.newBoolean( CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT_DESC).defaultValue(Boolean.TRUE.toString()); Map disallowAdditionalPropertiesIfNotPresentOpts = new HashMap<>(); disallowAdditionalPropertiesIfNotPresentOpts.put("false", "The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications."); disallowAdditionalPropertiesIfNotPresentOpts.put("true", "Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default."); disallowAdditionalPropertiesIfNotPresentOpt.setEnum(disallowAdditionalPropertiesIfNotPresentOpts); cliOptions.add(disallowAdditionalPropertiesIfNotPresentOpt); this.setDisallowAdditionalPropertiesIfNotPresent(true); CliOption enumUnknownDefaultCaseOpt = CliOption.newBoolean( CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue(Boolean.FALSE.toString()); Map enumUnknownDefaultCaseOpts = new HashMap<>(); enumUnknownDefaultCaseOpts.put("false", "No changes to the enum's are made, this is the default option."); enumUnknownDefaultCaseOpts.put("true", "With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case."); enumUnknownDefaultCaseOpt.setEnum(enumUnknownDefaultCaseOpts); cliOptions.add(enumUnknownDefaultCaseOpt); this.setEnumUnknownDefaultCase(false); // initialize special character mapping initializeSpecialCharacterMapping(); // Register common Mustache lambdas. registerMustacheLambdas(); } /** * Initialize special character mapping */ protected void initializeSpecialCharacterMapping() { // Initialize special characters specialCharReplacements.put("$", "Dollar"); specialCharReplacements.put("^", "Caret"); specialCharReplacements.put("|", "Pipe"); specialCharReplacements.put("=", "Equal"); specialCharReplacements.put("*", "Star"); specialCharReplacements.put("-", "Minus"); specialCharReplacements.put("&", "Ampersand"); specialCharReplacements.put("%", "Percent"); specialCharReplacements.put("#", "Hash"); specialCharReplacements.put("@", "At"); specialCharReplacements.put("!", "Exclamation"); specialCharReplacements.put("+", "Plus"); specialCharReplacements.put(":", "Colon"); specialCharReplacements.put(";", "Semicolon"); specialCharReplacements.put(">", "Greater_Than"); specialCharReplacements.put("<", "Less_Than"); specialCharReplacements.put(".", "Period"); specialCharReplacements.put("_", "Underscore"); specialCharReplacements.put("?", "Question_Mark"); specialCharReplacements.put(",", "Comma"); specialCharReplacements.put("'", "Quote"); specialCharReplacements.put("\"", "Double_Quote"); specialCharReplacements.put("/", "Slash"); specialCharReplacements.put("\\", "Back_Slash"); specialCharReplacements.put("(", "Left_Parenthesis"); specialCharReplacements.put(")", "Right_Parenthesis"); specialCharReplacements.put("{", "Left_Curly_Bracket"); specialCharReplacements.put("}", "Right_Curly_Bracket"); specialCharReplacements.put("[", "Left_Square_Bracket"); specialCharReplacements.put("]", "Right_Square_Bracket"); specialCharReplacements.put("~", "Tilde"); specialCharReplacements.put("`", "Backtick"); specialCharReplacements.put("<=", "Less_Than_Or_Equal_To"); specialCharReplacements.put(">=", "Greater_Than_Or_Equal_To"); specialCharReplacements.put("!=", "Not_Equal"); specialCharReplacements.put("<>", "Not_Equal"); specialCharReplacements.put("~=", "Tilde_Equal"); } /** * Return the symbol name of a symbol * * @param input Symbol (e.g. $) * @return Symbol name (e.g. Dollar) */ protected String getSymbolName(String input) { return specialCharReplacements.get(input); } /** * Return the example path * * @param path the path of the operation * @param operation OAS operation object * @return string presentation of the example path */ @Override @SuppressWarnings("static-method") public String generateExamplePath(String path, Operation operation) { StringBuilder sb = new StringBuilder(); sb.append(path); if (operation.getParameters() != null) { int count = 0; for (Parameter param : operation.getParameters()) { if (param instanceof QueryParameter) { StringBuilder paramPart = new StringBuilder(); QueryParameter qp = (QueryParameter) param; if (count == 0) { paramPart.append("?"); } else { paramPart.append(","); } count += 1; if (!param.getRequired()) { paramPart.append("["); } paramPart.append(param.getName()).append("="); paramPart.append("{"); // TODO support for multi, tsv? if (qp.getStyle() != null) { paramPart.append(param.getName()).append("1"); if (Parameter.StyleEnum.FORM.equals(qp.getStyle())) { if (qp.getExplode() != null && qp.getExplode()) { paramPart.append(","); } else { paramPart.append("&").append(param.getName()).append("="); paramPart.append(param.getName()).append("2"); } } else if (Parameter.StyleEnum.PIPEDELIMITED.equals(qp.getStyle())) { paramPart.append("|"); } else if (Parameter.StyleEnum.SPACEDELIMITED.equals(qp.getStyle())) { paramPart.append("%20"); } else { LOGGER.warn("query parameter '{}' style not support: {}", param.getName(), qp.getStyle()); } } else { paramPart.append(param.getName()); } paramPart.append("}"); if (!param.getRequired()) { paramPart.append("]"); } sb.append(paramPart); } } } return sb.toString(); } /** * Return the instantiation type of the property, especially for map and array * * @param schema property schema * @return string presentation of the instantiation type of the property */ public String toInstantiationType(Schema schema) { if (ModelUtils.isMapSchema(schema)) { Schema additionalProperties = ModelUtils.getAdditionalProperties(schema); String inner = getSchemaType(additionalProperties); return instantiationTypes.get("map") + ""; } else if (ModelUtils.isArraySchema(schema)) { ArraySchema arraySchema = (ArraySchema) schema; String inner = getSchemaType(getSchemaItems(arraySchema)); String parentType; if (ModelUtils.isSet(schema)) { parentType = "set"; } else { parentType = "array"; } return instantiationTypes.get(parentType) + "<" + inner + ">"; } else { return null; } } /** * Return the example value of the parameter. * * @param codegenParameter Codegen parameter */ public void setParameterExampleValue(CodegenParameter codegenParameter) { // set the example value // if not specified in x-example, generate a default value // 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)) { codegenParameter.example = "true"; } else if (Boolean.TRUE.equals(codegenParameter.isLong)) { codegenParameter.example = "789"; } else if (Boolean.TRUE.equals(codegenParameter.isInteger)) { codegenParameter.example = "56"; } else if (Boolean.TRUE.equals(codegenParameter.isFloat)) { codegenParameter.example = "3.4"; } else if (Boolean.TRUE.equals(codegenParameter.isDouble)) { codegenParameter.example = "1.2"; } else if (Boolean.TRUE.equals(codegenParameter.isNumber)) { codegenParameter.example = "8.14"; } else if (Boolean.TRUE.equals(codegenParameter.isBinary)) { codegenParameter.example = "BINARY_DATA_HERE"; } else if (Boolean.TRUE.equals(codegenParameter.isByteArray)) { codegenParameter.example = "BYTE_ARRAY_DATA_HERE"; } else if (Boolean.TRUE.equals(codegenParameter.isFile)) { codegenParameter.example = "/path/to/file.txt"; } else if (Boolean.TRUE.equals(codegenParameter.isDate)) { codegenParameter.example = "2013-10-20"; } else if (Boolean.TRUE.equals(codegenParameter.isDateTime)) { codegenParameter.example = "2013-10-20T19:20:30+01:00"; } else if (Boolean.TRUE.equals(codegenParameter.isUuid)) { codegenParameter.example = "38400000-8cf0-11bd-b23e-10b96e4ef00d"; } else if (Boolean.TRUE.equals(codegenParameter.isUri)) { codegenParameter.example = "https://openapi-generator.tech"; } else if (Boolean.TRUE.equals(codegenParameter.isString)) { codegenParameter.example = codegenParameter.paramName + "_example"; } else if (Boolean.TRUE.equals(codegenParameter.isFreeFormObject)) { codegenParameter.example = "Object"; } } /** * Return the example value of the parameter. * * @param codegenParameter Codegen parameter * @param parameter Parameter */ public void setParameterExampleValue(CodegenParameter codegenParameter, Parameter parameter) { if (parameter.getExample() != null) { codegenParameter.example = parameter.getExample().toString(); return; } if (parameter.getExamples() != null && !parameter.getExamples().isEmpty()) { Example example = parameter.getExamples().values().iterator().next(); if (example.getValue() != null) { codegenParameter.example = example.getValue().toString(); return; } } Schema schema = parameter.getSchema(); if (schema != null && schema.getExample() != null) { codegenParameter.example = schema.getExample().toString(); return; } setParameterExampleValue(codegenParameter); } /** * Return the examples of the parameter. * * @param codegenParameter Codegen parameter * @param parameter Parameter */ public void setParameterExamples(CodegenParameter codegenParameter, Parameter parameter) { if (parameter.getExamples() != null && !parameter.getExamples().isEmpty()) { codegenParameter.examples = parameter.getExamples(); } } /** * Return the example value of the parameter. * * @param codegenParameter Codegen parameter * @param requestBody Request body */ public void setParameterExampleValue(CodegenParameter codegenParameter, RequestBody requestBody) { Content content = requestBody.getContent(); if (content.size() > 1) { // @see ModelUtils.getSchemaFromContent() once(LOGGER).debug("Multiple MediaTypes found, using only the first one"); } MediaType mediaType = content.values().iterator().next(); if (mediaType.getExample() != null) { codegenParameter.example = mediaType.getExample().toString(); return; } if (mediaType.getExamples() != null && !mediaType.getExamples().isEmpty()) { Example example = mediaType.getExamples().values().iterator().next(); if (example.getValue() != null) { codegenParameter.example = example.getValue().toString(); return; } } setParameterExampleValue(codegenParameter); } /** * Sets the content type, style, and explode of the parameter based on the encoding specified * in the request body. * * @param codegenParameter Codegen parameter * @param mediaType MediaType from the request body */ public void setParameterEncodingValues(CodegenParameter codegenParameter, MediaType mediaType) { if (mediaType != null && mediaType.getEncoding() != null) { Encoding encoding = mediaType.getEncoding().get(codegenParameter.baseName); if (encoding != null) { boolean styleGiven = true; Encoding.StyleEnum style = encoding.getStyle(); if (style == null || style == Encoding.StyleEnum.FORM) { // (Unfortunately, swagger-parser-v3 will always provide 'form' // when style is not specified, so we can't detect that) style = Encoding.StyleEnum.FORM; styleGiven = false; } boolean explodeGiven = true; Boolean explode = encoding.getExplode(); if (explode == null) { explode = style == Encoding.StyleEnum.FORM; // Default to True when form, False otherwise explodeGiven = false; } if (!styleGiven && !explodeGiven) { // Ignore contentType if style or explode are specified. codegenParameter.contentType = encoding.getContentType(); } codegenParameter.style = style.toString(); codegenParameter.isDeepObject = Encoding.StyleEnum.DEEP_OBJECT == style; if (codegenParameter.isContainer) { codegenParameter.isExplode = explode; String collectionFormat = getCollectionFormat(codegenParameter); codegenParameter.collectionFormat = StringUtils.isEmpty(collectionFormat) ? "csv" : collectionFormat; codegenParameter.isCollectionFormatMulti = "multi".equals(collectionFormat); } else { codegenParameter.isExplode = false; codegenParameter.collectionFormat = null; codegenParameter.isCollectionFormatMulti = false; } } else { LOGGER.debug("encoding not specified for {}", codegenParameter.baseName); } } } /** * Return the example value of the property *

* This method should be overridden in the generator to meet its requirement. * * @param schema Property schema * @return string presentation of the example value of the property */ public String toExampleValue(Schema schema) { if (schema.getExample() != null) { return schema.getExample().toString(); } return "null"; } /** * Return the default value of the property *

* This method should be overridden in the generator to meet its requirement. * Return null if you do NOT want a default value. * Any non-null value will cause {{#defaultValue} check to pass. * * @param schema Property schema * @return string presentation of the default value of the property */ @SuppressWarnings("static-method") public String toDefaultValue(Schema schema) { if (schema.getDefault() != null) { return schema.getDefault().toString(); } return "null"; } /** * Return the default value of the parameter *

* Return null if you do NOT want a default value. * Any non-null value will cause {{#defaultValue} check to pass. * * @param schema Parameter schema * @return string presentation of the default value of the parameter */ public String toDefaultParameterValue(Schema schema) { // by default works as original method to be backward compatible return toDefaultValue(schema); } /** * Return the default value of the parameter *

* Return null if you do NOT want a default value. * Any non-null value will cause {{#defaultValue} check to pass. * * @param codegenProperty Codegen Property * @param schema Parameter schema * @return string presentation of the default value of the parameter */ public String toDefaultParameterValue(CodegenProperty codegenProperty, Schema schema) { // by default works as original method to be backward compatible return toDefaultParameterValue(schema); } /** * Return the property initialized from a data object * Useful for initialization with a plain object in Javascript * * @param name Name of the property object * @param schema Property schema * @return string presentation of the default value of the property */ @SuppressWarnings("static-method") public String toDefaultValueWithParam(String name, Schema schema) { return " = data." + name + ";"; } /** * Return the default value of the property *

* Return null if you do NOT want a default value. * Any non-null value will cause {{#defaultValue} check to pass. * * @param schema Property schema * @param codegenProperty Codegen property * @return string presentation of the default value of the property */ public String toDefaultValue(CodegenProperty codegenProperty, Schema schema) { // use toDefaultValue(schema) if generator has not overriden this method return toDefaultValue(schema); } /** * returns the OpenAPI type for the property. Use getAlias to handle $ref of primitive type * * @param schema property schema * @return string presentation of the type **/ @SuppressWarnings("static-method") public String getSchemaType(Schema schema) { if (ModelUtils.isComposedSchema(schema)) { // composed schema // Get the interfaces, i.e. the set of elements under 'allOf', 'anyOf' or 'oneOf'. List schemas = ModelUtils.getInterfaces(schema); List names = new ArrayList<>(); // Build a list of the schema types under each interface. // For example, if a 'allOf' composed schema has $ref children, // add the type of each child to the list of names. for (Schema s : schemas) { names.add(getSingleSchemaType(s)); } if (schema.getAllOf() != null) { return toAllOfName(names, schema); } else if (schema.getAnyOf() != null) { // anyOf return toAnyOfName(names, schema); } else if (schema.getOneOf() != null) { // oneOf return toOneOfName(names, schema); } } return getSingleSchemaType(schema); } protected Schema getSchemaItems(ArraySchema schema) { Schema items = schema.getItems(); if (items == null) { LOGGER.error("Undefined array inner type for `{}`. Default to String.", schema.getName()); items = new StringSchema().description("TODO default missing array inner type to string"); schema.setItems(items); } return items; } protected Schema getSchemaAdditionalProperties(Schema schema) { Schema inner = ModelUtils.getAdditionalProperties(schema); if (inner == null) { LOGGER.error("`{}` (map property) does not have a proper inner type defined. Default to type:string", schema.getName()); inner = new StringSchema().description("TODO default missing map inner type to string"); schema.setAdditionalProperties(inner); } return inner; } /** * Return the name of the 'allOf' composed schema. * * @param names List of names * @param composedSchema composed schema * @return name of the allOf schema */ @SuppressWarnings("static-method") public String toAllOfName(List names, Schema composedSchema) { Map exts = composedSchema.getExtensions(); if (exts != null && exts.containsKey("x-all-of-name")) { return (String) exts.get("x-all-of-name"); } if (names.size() == 0) { LOGGER.error("allOf has no member defined: {}. Default to ERROR_ALLOF_SCHEMA", composedSchema); return "ERROR_ALLOF_SCHEMA"; } else if (names.size() == 1) { return names.get(0); } else { LOGGER.debug("allOf with multiple schemas defined. Using only the first one: {}", names.get(0)); return names.get(0); } } /** * Return the name of the anyOf schema * * @param names List of names * @param composedSchema composed schema * @return name of the anyOf schema */ @SuppressWarnings("static-method") public String toAnyOfName(List names, Schema composedSchema) { return "anyOf<" + String.join(",", names) + ">"; } /** * Return the name of the oneOf schema. *

* This name is used to set the value of CodegenProperty.openApiType. *

* If the 'x-one-of-name' extension is specified in the OAS document, return that value. * Otherwise, a name is constructed by creating a comma-separated list of all the names * of the oneOf schemas. * * @param names List of names * @param composedSchema composed schema * @return name of the oneOf schema */ @SuppressWarnings("static-method") public String toOneOfName(List names, Schema composedSchema) { Map exts = composedSchema.getExtensions(); if (exts != null && exts.containsKey("x-one-of-name")) { return (String) exts.get("x-one-of-name"); } return "oneOf<" + String.join(",", names) + ">"; } @Override public Schema unaliasSchema(Schema schema) { return ModelUtils.unaliasSchema(this.openAPI, schema, schemaMapping); } /** * Return a string representation of the schema type, resolving aliasing and references if necessary. * * @param schema input * @return the string representation of the schema type. */ protected String getSingleSchemaType(Schema schema) { Schema unaliasSchema = unaliasSchema(schema); if (StringUtils.isNotBlank(unaliasSchema.get$ref())) { // reference to another definition/schema // get the schema/model name from $ref String schemaName = ModelUtils.getSimpleRef(unaliasSchema.get$ref()); if (StringUtils.isNotEmpty(schemaName)) { if (schemaMapping.containsKey(schemaName)) { return schemaName; } return getAlias(schemaName); } else { LOGGER.warn("Error obtaining the datatype from ref: {}. Default to 'object'", unaliasSchema.get$ref()); return "object"; } } else { // primitive type or model return getAlias(getPrimitiveType(unaliasSchema)); } } /** * Return the OAI type (e.g. integer, long, etc) corresponding to a schema. *

$ref
is not taken into account by this method. *

* If the schema is free-form (i.e. 'type: object' with no properties) or inline * schema, the returned OAI type is 'object'. * * @param schema * @return type */ private String getPrimitiveType(Schema schema) { if (schema == null) { throw new RuntimeException("schema cannot be null in getPrimitiveType"); } else if (typeMapping.containsKey(schema.getType() + "+" + schema.getFormat())) { // allows custom type_format mapping. // use {type}+{format} return typeMapping.get(schema.getType() + "+" + schema.getFormat()); } else if (ModelUtils.isNullType(schema)) { // The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x, // though this tooling supports it. return "null"; } else if (ModelUtils.isDecimalSchema(schema)) { // special handle of type: string, format: number return "decimal"; } else if (ModelUtils.isByteArraySchema(schema)) { return "ByteArray"; } else if (ModelUtils.isFileSchema(schema)) { return "file"; } else if (ModelUtils.isBinarySchema(schema)) { return SchemaTypeUtil.BINARY_FORMAT; } else if (ModelUtils.isBooleanSchema(schema)) { return SchemaTypeUtil.BOOLEAN_TYPE; } else if (ModelUtils.isDateSchema(schema)) { return SchemaTypeUtil.DATE_FORMAT; } else if (ModelUtils.isDateTimeSchema(schema)) { return "DateTime"; } else if (ModelUtils.isNumberSchema(schema)) { if (schema.getFormat() == null) { // no format defined return "number"; } else if (ModelUtils.isFloatSchema(schema)) { return SchemaTypeUtil.FLOAT_FORMAT; } else if (ModelUtils.isDoubleSchema(schema)) { return SchemaTypeUtil.DOUBLE_FORMAT; } else { LOGGER.warn("Unknown `format` {} detected for type `number`. Defaulting to `number`", schema.getFormat()); return "number"; } } else if (ModelUtils.isIntegerSchema(schema)) { if (ModelUtils.isUnsignedLongSchema(schema)) { return "UnsignedLong"; } else if (ModelUtils.isUnsignedIntegerSchema(schema)) { return "UnsignedInteger"; } else if (ModelUtils.isLongSchema(schema)) { return "long"; } else if (ModelUtils.isShortSchema(schema)) {// int32 return "integer"; } else { return schema.getType(); // integer } } else if (ModelUtils.isMapSchema(schema)) { return "map"; } else if (ModelUtils.isArraySchema(schema)) { if (ModelUtils.isSet(schema)) { return "set"; } else { return "array"; } } else if (ModelUtils.isUUIDSchema(schema)) { return "UUID"; } else if (ModelUtils.isURISchema(schema)) { return "URI"; } else if (ModelUtils.isStringSchema(schema)) { if (typeMapping.containsKey(schema.getFormat())) { // If the format matches a typeMapping (supplied with the --typeMappings flag) // then treat the format as a primitive type. // This allows the typeMapping flag to add a new custom type which can then // be used in the format field. return schema.getFormat(); } return "string"; } else if (ModelUtils.isFreeFormObject(schema)) { // Note: the value of a free-form object cannot be an arbitrary type. Per OAS specification, // it must be a map of string to values. return "object"; } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // having property implies it's a model return "object"; } else if (ModelUtils.isAnyType(schema)) { return "AnyType"; } else if (StringUtils.isNotEmpty(schema.getType())) { if (!schemaMapping.containsKey(schema.getType())) { LOGGER.warn("Unknown type found in the schema: {}. To map it, please use the schema mapping option (e.g. --schema-mappings in CLI)", schema.getType()); } return schema.getType(); } // The 'type' attribute has not been set in the OAS schema, which means the value // can be an arbitrary type, e.g. integer, string, object, array, number... // TODO: we should return a different value to distinguish between free-form object // and arbitrary type. return "object"; } /** * Return the lowerCamelCase of the string * * @param name string to be lowerCamelCased * @return lowerCamelCase string */ @SuppressWarnings("static-method") public String lowerCamelCase(String name) { return (name.length() > 0) ? (Character.toLowerCase(name.charAt(0)) + name.substring(1)) : ""; } /** * Output the language-specific type declaration of a given OAS name. * * @param name name * @return a string presentation of the type */ @Override @SuppressWarnings("static-method") public String getTypeDeclaration(String name) { return name; } /** * Output the language-specific type declaration of the property. * * @param schema property schema * @return a string presentation of the property type */ @Override public String getTypeDeclaration(Schema schema) { if (schema == null) { LOGGER.warn("Null schema found. Default type to `NULL_SCHEMA_ERR`"); return "NULL_SCHEMA_ERR"; } String oasType = getSchemaType(schema); if (typeMapping.containsKey(oasType)) { return typeMapping.get(oasType); } return oasType; } /** * Determine the type alias for the given type if it exists. This feature * was originally developed for Java because the language does not have an aliasing * mechanism of its own but later extends to handle other languages * * @param name The type name. * @return The alias of the given type, if it exists. If there is no alias * for this type, then returns the input type name. */ public String getAlias(String name) { if (typeAliases != null && typeAliases.containsKey(name)) { return typeAliases.get(name); } return name; } /** * Output the Getter name for boolean property, e.g. getActive * * @param name the name of the property * @return getter name based on naming convention */ @Override public String toBooleanGetter(String name) { return "get" + getterAndSetterCapitalize(name); } /** * Output the Getter name, e.g. getSize * * @param name the name of the property * @return getter name based on naming convention */ @Override public String toGetter(String name) { return "get" + getterAndSetterCapitalize(name); } /** * Output the Setter name, e.g. setSize * * @param name the name of the property * @return setter name based on naming convention */ @Override public String toSetter(String name) { return "set" + getterAndSetterCapitalize(name); } /** * Output the API (class) name (capitalized) ending with the specified or default suffix * Return DefaultApi if name is empty * * @param name the name of the Api * @return capitalized Api name */ @Override public String toApiName(String name) { if (name.length() == 0) { return "DefaultApi"; } return camelize(apiNamePrefix + "_" + name + "_" + apiNameSuffix); } /** * Converts the OpenAPI schema name to a model name suitable for the current code generator. * May be overridden for each programming language. * In case the name belongs to the TypeSystem it won't be renamed. * * @param name the name of the model * @return capitalized model name */ @Override public String toModelName(final String name) { // obtain the name from modelNameMapping directly if provided if (modelNameMapping.containsKey(name)) { return modelNameMapping.get(name); } if (schemaKeyToModelNameCache.containsKey(name)) { return schemaKeyToModelNameCache.get(name); } String camelizedName = camelize(modelNamePrefix + "_" + name + "_" + modelNameSuffix); schemaKeyToModelNameCache.put(name, camelizedName); return camelizedName; } private static class NamedSchema { private NamedSchema(String name, Schema s, boolean required, boolean schemaIsFromAdditionalProperties) { this.name = name; this.schema = s; this.required = required; this.schemaIsFromAdditionalProperties = schemaIsFromAdditionalProperties; } private String name; private Schema schema; private boolean required; private boolean schemaIsFromAdditionalProperties; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) 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); } @Override public int hashCode() { return Objects.hash(name, schema, required, schemaIsFromAdditionalProperties); } } Map schemaCodegenPropertyCache = new HashMap<>(); protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map allDefinitions) { final Schema composed = schema; Map properties = new LinkedHashMap<>(); List required = new ArrayList<>(); Map allProperties = new LinkedHashMap<>(); List allRequired = new ArrayList<>(); // if schema has properties outside of allOf/oneOf/anyOf also add them to m if (composed.getProperties() != null && !composed.getProperties().isEmpty()) { if (composed.getOneOf() != null && !composed.getOneOf().isEmpty()) { LOGGER.warn("'oneOf' is intended to include only the additional optional OAS extension discriminator object. " + "For more details, see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'."); } addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null); } // parent model final String parentName = ModelUtils.getParentName(composed, allDefinitions); final List allParents = ModelUtils.getAllParentsName(composed, allDefinitions, false); final Schema parent = StringUtils.isBlank(parentName) || allDefinitions == null ? null : allDefinitions.get(parentName); // TODO revise the logic below to set discriminator, xml attributes if (supportsInheritance || supportsMixins) { m.allVars = new ArrayList<>(); if (composed.getAllOf() != null) { int modelImplCnt = 0; // only one inline object allowed in a ComposedModel int modelDiscriminators = 0; // only one discriminator allowed in a ComposedModel for (Object innerSchema : composed.getAllOf()) { // TODO need to work with anyOf, oneOf as well if (m.discriminator == null && ((Schema) innerSchema).getDiscriminator() != null) { LOGGER.debug("discriminator is set to null (not correctly set earlier): {}", m.name); m.setDiscriminator(createDiscriminator(m.name, (Schema) innerSchema)); modelDiscriminators++; } if (((Schema) innerSchema).getXml() != null) { m.xmlPrefix = ((Schema) innerSchema).getXml().getPrefix(); m.xmlNamespace = ((Schema) innerSchema).getXml().getNamespace(); m.xmlName = ((Schema) innerSchema).getXml().getName(); } if (modelDiscriminators > 1) { LOGGER.error("Allof composed schema is inheriting >1 discriminator. Only use one discriminator: {}", composed); } if (modelImplCnt++ > 1) { LOGGER.warn("More than one inline schema specified in allOf:. Only the first one is recognized. All others are ignored."); break; // only one schema with discriminator allowed in allOf } } } } // interfaces (schemas defined in allOf, anyOf, oneOf) List interfaces = ModelUtils.getInterfaces(composed); if (!interfaces.isEmpty()) { // m.interfaces is for backward compatibility if (m.interfaces == null) m.interfaces = new ArrayList<>(); for (Schema interfaceSchema : interfaces) { interfaceSchema = unaliasSchema(interfaceSchema); if (StringUtils.isBlank(interfaceSchema.get$ref())) { // primitive type String languageType = getTypeDeclaration(interfaceSchema); CodegenProperty interfaceProperty = fromProperty(languageType, interfaceSchema, false); if (ModelUtils.isArraySchema(interfaceSchema) || ModelUtils.isMapSchema(interfaceSchema)) { while (interfaceProperty != null) { addImport(m, interfaceProperty.complexType); interfaceProperty = interfaceProperty.items; } } if (composed.getAnyOf() != null) { if (m.anyOf.contains(languageType)) { LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); } else { m.anyOf.add(languageType); } } else if (composed.getOneOf() != null) { if (m.oneOf.contains(languageType)) { LOGGER.debug("{} (oneOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); } else { m.oneOf.add(languageType); } } else if (composed.getAllOf() != null) { // no need to add primitive type to allOf, which should comprise of schemas (models) only } else { LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } continue; } // the rest of the section is for model Schema refSchema = null; String ref = ModelUtils.getSimpleRef(interfaceSchema.get$ref()); if (allDefinitions != null) { refSchema = allDefinitions.get(ref); } final String modelName = toModelName(ref); CodegenProperty interfaceProperty = fromProperty(modelName, interfaceSchema, false); m.interfaces.add(modelName); addImport(composed, refSchema, m, modelName); if (allDefinitions != null && refSchema != null) { if (allParents.contains(ref) && supportsMultipleInheritance) { // multiple inheritance addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { // single inheritance addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } else { // composition Map newProperties = new LinkedHashMap<>(); addProperties(newProperties, required, refSchema, new HashSet<>()); mergeProperties(properties, newProperties); addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } } if (composed.getAnyOf() != null) { m.anyOf.add(modelName); } else if (composed.getOneOf() != null) { m.oneOf.add(modelName); } else if (composed.getAllOf() != null) { m.allOf.add(modelName); } else { LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } } } if (parent != null && composed.getAllOf() != null) { // set parent for allOf only m.parentSchema = parentName; m.parent = toModelName(parentName); if (supportsMultipleInheritance) { m.allParents = new ArrayList<>(); for (String pname : allParents) { String pModelName = toModelName(pname); m.allParents.add(pModelName); addImport(m, pModelName); } } else { // single inheritance addImport(m, m.parent); } } // child schema (properties owned by the schema itself) for (Schema component : interfaces) { if (component.get$ref() == null) { if (component != null) { // component is the child schema addProperties(properties, required, component, new HashSet<>()); // includes child's properties (all, required) in allProperties, allRequired addProperties(allProperties, allRequired, component, new HashSet<>()); } // in 7.0.0 release, we comment out below to allow more than 1 child schemas in allOf //break; // at most one child only } } if (composed.getRequired() != null) { required.addAll(composed.getRequired()); allRequired.addAll(composed.getRequired()); } addVars(m, unaliasPropertySchema(properties), required, unaliasPropertySchema(allProperties), allRequired); // Per OAS specification, composed schemas may use the 'additionalProperties' keyword. if (supportsAdditionalPropertiesWithComposedSchema) { // Process the schema specified with the 'additionalProperties' keyword. // This will set the 'CodegenModel.additionalPropertiesType' field // and potentially 'Codegen.parent'. // // Note: it's not a good idea to use single class inheritance to implement // the 'additionalProperties' keyword. Code generators that use single class // inheritance sometimes use the 'Codegen.parent' field to implement the // 'additionalProperties' keyword. However, that would be in conflict with // 'allOf' composed schemas, because these code generators also want to set // 'Codegen.parent' to the first child schema of the 'allOf' schema. addAdditionPropertiesToCodeGenModel(m, schema); } if (Boolean.TRUE.equals(schema.getNullable())) { m.isNullable = Boolean.TRUE; } // end of code block for composed schema } /** * Combines all previously-detected type entries for a schema with newly-discovered ones, to ensure * that schema for items like enum include all possible values. */ private void mergeProperties(Map existingProperties, Map newProperties) { // https://github.com/OpenAPITools/openapi-generator/issues/12545 if (null != existingProperties && null != newProperties) { Schema existingType = existingProperties.get("type"); Schema newType = newProperties.get("type"); newProperties.forEach((key, value) -> existingProperties.put( key, AnnotationsUtils.clone(value, specVersionGreaterThanOrEqualTo310(openAPI)) )); if (null != existingType && null != newType && null != newType.getEnum() && !newType.getEnum().isEmpty()) { for (Object e : newType.getEnum()) { // ensure all interface enum types are added to schema if (null != existingType.getEnum() && !existingType.getEnum().contains(e)) { existingType.addEnumItemObject(e); } } existingProperties.put("type", existingType); } } } protected void updateModelForObject(CodegenModel m, Schema schema) { if (schema.getProperties() != null || schema.getRequired() != null && !(ModelUtils.isComposedSchema(schema))) { // passing null to allProperties and allRequired as there's no parent addVars(m, unaliasPropertySchema(schema.getProperties()), schema.getRequired(), null, null); } if (ModelUtils.isMapSchema(schema)) { // an object or anyType composed schema that has additionalProperties set addAdditionPropertiesToCodeGenModel(m, schema); } else if (ModelUtils.isFreeFormObject(schema)) { // non-composed object type with no properties + additionalProperties // additionalProperties must be null, ObjectSchema, or empty Schema addAdditionPropertiesToCodeGenModel(m, schema); } // process 'additionalProperties' setAddProps(schema, m); addRequiredVarsMap(schema, m); } protected void updateModelForAnyType(CodegenModel m, Schema schema) { // The 'null' value is allowed when the OAS schema is 'any type'. // See https://github.com/OAI/OpenAPI-Specification/issues/1389 if (Boolean.FALSE.equals(schema.getNullable())) { LOGGER.error("Schema '{}' is any type, which includes the 'null' value. 'nullable' cannot be set to 'false'", m.name); } // m.isNullable = true; if (ModelUtils.isMapSchema(schema)) { // an object or anyType composed schema that has additionalProperties set addAdditionPropertiesToCodeGenModel(m, schema); m.isMap = true; } if (schema.getProperties() != null || schema.getRequired() != null && !(ModelUtils.isComposedSchema(schema))) { // passing null to allProperties and allRequired as there's no parent addVars(m, unaliasPropertySchema(schema.getProperties()), schema.getRequired(), null, null); } // process 'additionalProperties' setAddProps(schema, m); addRequiredVarsMap(schema, m); } protected String toTestCaseName(String specTestCaseName) { return specTestCaseName; } /** * A method that allows generators to pre-process test example payloads * This can be useful if one needs to change how values like null in string are represented * @param data the test data payload * @return the updated test data payload */ protected Object processTestExampleData(Object data) { return data; } /** * Processes any test cases if they exist in the components.x-test-examples vendor extensions * If they exist then cast them to java class instances and return them back in a map * @param refToTestCases the component schema name that the test cases are for */ private HashMap extractSchemaTestCases(String refToTestCases) { // schemaName to a map of test case name to test case HashMap vendorExtensions = (HashMap) openAPI.getComponents().getExtensions(); if (vendorExtensions == null || !vendorExtensions.containsKey(xSchemaTestExamplesKey)) { return null; } if (!refToTestCases.startsWith(xSchemaTestExamplesRefPrefix)) { return null; } String schemaName = refToTestCases.substring(xSchemaTestExamplesRefPrefix.length()); HashMap schemaTestCases = new HashMap<>(); LinkedHashMap schemaNameToTestCases = (LinkedHashMap) vendorExtensions.get(xSchemaTestExamplesKey); if (!schemaNameToTestCases.containsKey(schemaName)) { return null; } LinkedHashMap> testNameToTesCase = (LinkedHashMap>) schemaNameToTestCases.get(schemaName); for (Entry> entry : testNameToTesCase.entrySet()) { LinkedHashMap testExample = (LinkedHashMap) entry.getValue(); String nameInSnakeCase = toTestCaseName(entry.getKey()); Object data = processTestExampleData(testExample.get("data")); SchemaTestCase testCase = new SchemaTestCase( (String) testExample.getOrDefault("description", ""), new ObjectWithTypeBooleans(data), (boolean) testExample.get("valid") ); schemaTestCases.put(nameInSnakeCase, testCase); } return schemaTestCases; } /** * Sets the booleans that define the model's type * * @param model the model to update * @param schema the model's schema */ protected void updateModelForString(CodegenModel model, Schema schema) { // NOTE: String schemas as CodegenModel is a rare use case and may be removed at a later date. if (ModelUtils.isDateTimeSchema(schema)) { // NOTE: DateTime schemas as CodegenModel is a rare use case and may be removed at a later date. model.setIsString(false); // for backward compatibility with 2.x model.isDateTime = Boolean.TRUE; } else if (ModelUtils.isDateSchema(schema)) { // NOTE: Date schemas as CodegenModel is a rare use case and may be removed at a later date. model.setIsString(false); // for backward compatibility with 2.x model.isDate = Boolean.TRUE; } else if (ModelUtils.isUUIDSchema(schema)) { // NOTE: UUID schemas as CodegenModel is a rare use case and may be removed at a later date. model.setIsString(false); model.setIsUuid(true); } else if (ModelUtils.isURISchema(schema)) { model.setIsString(false); model.setIsUri(true); } } protected void updateModelForNumber(CodegenModel model, Schema schema) { // NOTE: Number schemas as CodegenModel is a rare use case and may be removed at a later date. model.isNumeric = Boolean.TRUE; if (ModelUtils.isFloatSchema(schema)) { // float model.isFloat = Boolean.TRUE; } else if (ModelUtils.isDoubleSchema(schema)) { // double model.isDouble = Boolean.TRUE; } } protected void updateModelForInteger(CodegenModel model, Schema schema) { // NOTE: Integral schemas as CodegenModel is a rare use case and may be removed at a later date. model.isNumeric = Boolean.TRUE; if (ModelUtils.isLongSchema(schema)) { // int64/long format model.isLong = Boolean.TRUE; } else { model.isInteger = Boolean.TRUE; // older use case, int32 and unbounded int if (ModelUtils.isShortSchema(schema)) { // int32 model.setIsShort(Boolean.TRUE); } } } /** * Convert OAS Model object to Codegen Model object. * * @param name the name of the model * @param schema OAS Model object * @return Codegen Model object */ @Override public CodegenModel fromModel(String name, Schema schema) { Map allDefinitions = ModelUtils.getSchemas(this.openAPI); CodegenModel m = CodegenModelFactory.newInstance(CodegenModelType.MODEL); if (schema.equals(trueSchema)) { m.setIsBooleanSchemaTrue(true); } else if (schema.equals(falseSchema)) { m.setIsBooleanSchemaFalse(true); } // unalias schema schema = unaliasSchema(schema); if (schema == null) { LOGGER.warn("Schema {} not found", name); return null; } ModelUtils.syncValidationProperties(schema, m); if (openAPI != null) { HashMap schemaTestCases = extractSchemaTestCases(xSchemaTestExamplesRefPrefix + name); m.testCases = schemaTestCases; } if (reservedWords.contains(name)) { m.name = escapeReservedWord(name); } else { m.name = name; } m.title = escapeText(schema.getTitle()); m.description = escapeText(schema.getDescription()); m.unescapedDescription = schema.getDescription(); m.classname = toModelName(name); m.classVarName = toVarName(name); m.classFilename = toModelFilename(name); m.modelJson = Json.pretty(schema); m.externalDocumentation = schema.getExternalDocs(); if (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) { m.getVendorExtensions().putAll(schema.getExtensions()); } m.isAlias = (typeAliases.containsKey(name) || isAliasOfSimpleTypes(schema)); // check if the unaliased schema is an alias of simple OAS types m.setDiscriminator(createDiscriminator(name, schema)); if (schema.getDeprecated() != null) { m.isDeprecated = schema.getDeprecated(); } if (schema.getXml() != null) { m.xmlPrefix = schema.getXml().getPrefix(); m.xmlNamespace = schema.getXml().getNamespace(); m.xmlName = schema.getXml().getName(); } if (!ModelUtils.isAnyType(schema) && !ModelUtils.isTypeObjectSchema(schema) && !ModelUtils.isArraySchema(schema) && schema.get$ref() == null && schema.getEnum() != null && !schema.getEnum().isEmpty()) { // TODO remove the anyType check here in the future ANyType models can have enums defined m.isEnum = true; // comment out below as allowableValues is not set in post processing model enum m.allowableValues = new HashMap<>(); m.allowableValues.put("values", schema.getEnum()); } if (!ModelUtils.isArraySchema(schema)) { m.dataType = getSchemaType(schema); } if (!ModelUtils.isAnyType(schema) && Boolean.TRUE.equals(schema.getNullable())) { m.isNullable = Boolean.TRUE; } m.setTypeProperties(schema); m.setFormat(schema.getFormat()); m.setComposedSchemas(getComposedSchemas(schema)); if (ModelUtils.isArraySchema(schema)) { CodegenProperty arrayProperty = fromProperty(name, schema, false); m.setItems(arrayProperty.items); m.arrayModelType = arrayProperty.complexType; addParentContainer(m, name, schema); } else if (ModelUtils.isIntegerSchema(schema)) { // integer type updateModelForInteger(m, schema); } else if (ModelUtils.isStringSchema(schema)) { updateModelForString(m, schema); } else if (ModelUtils.isNumberSchema(schema)) { updateModelForNumber(m, schema); } else if (ModelUtils.isAnyType(schema)) { updateModelForAnyType(m, schema); } else if (ModelUtils.isTypeObjectSchema(schema)) { updateModelForObject(m, schema); } else if (!ModelUtils.isNullType(schema)) { // referenced models here, component that refs another component which is a model // if a component references a schema which is not a generated model, the refed schema will be loaded into // schema by unaliasSchema and one of the above code paths will be taken } if (schema.get$ref() != null) { m.setRef(schema.get$ref()); } if (ModelUtils.isComposedSchema(schema)) { updateModelForComposedSchema(m, schema, allDefinitions); } // remove duplicated properties m.removeAllDuplicatedProperty(); // set isDiscriminator on the discriminator property if (m.discriminator != null) { String discPropName = m.discriminator.getPropertyBaseName(); List> listOLists = new ArrayList<>(); listOLists.add(m.requiredVars); listOLists.add(m.vars); listOLists.add(m.allVars); for (List theseVars : listOLists) { for (CodegenProperty requiredVar : theseVars) { if (discPropName.equals(requiredVar.baseName)) { requiredVar.isDiscriminator = true; } } } } if (m.requiredVars != null && m.requiredVars.size() > 0) { m.setHasRequired(true); } if (sortModelPropertiesByRequiredFlag) { Comparator comparator = new Comparator() { @Override public int compare(CodegenProperty one, CodegenProperty another) { if (one.required == another.required) return 0; else if (one.required) return -1; else return 1; } }; Collections.sort(m.vars, comparator); Collections.sort(m.allVars, comparator); } // post process model properties if (m.vars != null) { for (CodegenProperty prop : m.vars) { postProcessModelProperty(m, prop); } m.hasVars = m.vars.size() > 0; } if (m.allVars != null) { for (CodegenProperty prop : m.allVars) { postProcessModelProperty(m, prop); } } return m; } protected void setAddProps(Schema schema, IJsonSchemaValidationProperties property) { if (schema.equals(new Schema())) { // if we are trying to set additionalProperties on an empty schema stop recursing return; } // Note: This flag is set to true if additioanl properties // is set (any type, free form object, boolean true, string, etc). // The variable name may be renamed later to avoid confusion. boolean additionalPropertiesIsAnyType = false; CodegenModel m = null; if (property instanceof CodegenModel) { m = (CodegenModel) property; } CodegenProperty addPropProp = null; boolean isAdditionalPropertiesTrue = false; if (schema.getAdditionalProperties() == null) { if (!disallowAdditionalPropertiesIfNotPresent) { isAdditionalPropertiesTrue = true; addPropProp = fromProperty(getAdditionalPropertiesName(), new Schema(), false); additionalPropertiesIsAnyType = true; } } else if (schema.getAdditionalProperties() instanceof Boolean) { if (Boolean.TRUE.equals(schema.getAdditionalProperties())) { isAdditionalPropertiesTrue = true; addPropProp = fromProperty(getAdditionalPropertiesName(), new Schema(), false); additionalPropertiesIsAnyType = true; } } else { // if additioanl properties is set (e.g. free form object, any type, string, etc) addPropProp = fromProperty(getAdditionalPropertiesName(), (Schema) schema.getAdditionalProperties(), false); additionalPropertiesIsAnyType = true; } if (additionalPropertiesIsAnyType) { property.setAdditionalPropertiesIsAnyType(true); } if (m != null && (isAdditionalPropertiesTrue || additionalPropertiesIsAnyType)) { m.isAdditionalPropertiesTrue = true; } if (ModelUtils.isComposedSchema(schema) && !supportsAdditionalPropertiesWithComposedSchema) { return; } if (addPropProp != null) { property.setAdditionalProperties(addPropProp); } } /** * Recursively look in Schema sc for the discriminator discPropName * and return a CodegenProperty with the dataType and required params set * the returned CodegenProperty may not be required and it may not be of type string * * @param composedSchemaName The name of the sc Schema * @param sc The Schema that may contain the discriminator * @param discPropName The String that is the discriminator propertyName in the schema * @param visitedSchemas A set of visited schema names * */ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, String discPropName, Set visitedSchemas) { if (visitedSchemas.contains(composedSchemaName)) { // recursive schema definition found return null; } else { visitedSchemas.add(composedSchemaName); } Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc); if (refSchema.getProperties() != null && refSchema.getProperties().get(discPropName) != null) { Schema discSchema = (Schema) refSchema.getProperties().get(discPropName); CodegenProperty cp = new CodegenProperty(); if (ModelUtils.isStringSchema(discSchema)) { cp.isString = true; } cp.setRequired(false); if (refSchema.getRequired() != null && refSchema.getRequired().contains(discPropName)) { cp.setRequired(true); } return cp; } if (ModelUtils.isComposedSchema(refSchema)) { Schema composedSchema = refSchema; if (composedSchema.getAllOf() != null) { // If our discriminator is in one of the allOf schemas break when we find it for (Object allOf : composedSchema.getAllOf()) { CodegenProperty cp = discriminatorFound(composedSchemaName, (Schema) allOf, discPropName, visitedSchemas); if (cp != null) { return cp; } } } if (composedSchema.getOneOf() != null && composedSchema.getOneOf().size() != 0) { // All oneOf definitions must contain the discriminator CodegenProperty cp = new CodegenProperty(); for (Object oneOf : composedSchema.getOneOf()) { String modelName = ModelUtils.getSimpleRef(((Schema) oneOf).get$ref()); CodegenProperty thisCp = discriminatorFound(composedSchemaName, (Schema) oneOf, discPropName, visitedSchemas); if (thisCp == null) { LOGGER.warn( "'{}' defines discriminator '{}', but the referenced OneOf schema '{}' is missing {}", composedSchemaName, discPropName, modelName, discPropName); } if (cp != null && cp.dataType == null) { cp = thisCp; continue; } if (cp != thisCp) { LOGGER.warn( "'{}' defines discriminator '{}', but the OneOf schema '{}' has a different {} definition than the prior OneOf schema's. Make sure the {} type and required values are the same", composedSchemaName, discPropName, modelName, discPropName, discPropName); } } return cp; } if (composedSchema.getAnyOf() != null && composedSchema.getAnyOf().size() != 0) { // All anyOf definitions must contain the discriminator because a min of one must be selected CodegenProperty cp = new CodegenProperty(); for (Object anyOf : composedSchema.getAnyOf()) { String modelName = ModelUtils.getSimpleRef(((Schema) anyOf).get$ref()); CodegenProperty thisCp = discriminatorFound(composedSchemaName, (Schema) anyOf, discPropName, visitedSchemas); if (thisCp == null) { LOGGER.warn( "'{}' defines discriminator '{}', but the referenced AnyOf schema '{}' is missing {}", composedSchemaName, discPropName, modelName, discPropName); } if (cp != null && cp.dataType == null) { cp = thisCp; continue; } if (cp != thisCp) { LOGGER.warn( "'{}' defines discriminator '{}', but the AnyOf schema '{}' has a different {} definition than the prior AnyOf schema's. Make sure the {} type and required values are the same", composedSchemaName, discPropName, modelName, discPropName, discPropName); } } return cp; } } return null; } /** * Recursively look in Schema sc for the discriminator and return it * * @param sc The Schema that may contain the discriminator * @param visitedSchemas An array list of visited schemas */ private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList visitedSchemas) { Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc); Discriminator foundDisc = refSchema.getDiscriminator(); if (foundDisc != null) { return foundDisc; } if (this.getLegacyDiscriminatorBehavior()) { return null; } for (Schema s : visitedSchemas) { if (s == refSchema) { return null; } } visitedSchemas.add(refSchema); Discriminator disc = new Discriminator(); if (ModelUtils.isComposedSchema(refSchema)) { Schema composedSchema = refSchema; if (composedSchema.getAllOf() != null) { // If our discriminator is in one of the allOf schemas break when we find it for (Object allOf : composedSchema.getAllOf()) { foundDisc = recursiveGetDiscriminator((Schema) allOf, visitedSchemas); if (foundDisc != null) { disc.setPropertyName(foundDisc.getPropertyName()); disc.setMapping(foundDisc.getMapping()); return disc; } } } if (composedSchema.getOneOf() != null && composedSchema.getOneOf().size() != 0) { // All oneOf definitions must contain the discriminator Integer hasDiscriminatorCnt = 0; Integer hasNullTypeCnt = 0; Set discriminatorsPropNames = new HashSet<>(); for (Object oneOf : composedSchema.getOneOf()) { if (ModelUtils.isNullType((Schema) oneOf)) { // The null type does not have a discriminator. Skip. hasNullTypeCnt++; continue; } foundDisc = recursiveGetDiscriminator((Schema) oneOf, visitedSchemas); if (foundDisc != null) { discriminatorsPropNames.add(foundDisc.getPropertyName()); hasDiscriminatorCnt++; } } if (discriminatorsPropNames.size() > 1) { 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()); disc.setMapping(foundDisc.getMapping()); return disc; } // If the scenario when oneOf has two children and one of them is the 'null' type, // there is no need for a discriminator. } if (composedSchema.getAnyOf() != null && composedSchema.getAnyOf().size() != 0) { // All anyOf definitions must contain the discriminator because a min of one must be selected Integer hasDiscriminatorCnt = 0; Integer hasNullTypeCnt = 0; Set discriminatorsPropNames = new HashSet<>(); for (Object anyOf : composedSchema.getAnyOf()) { if (ModelUtils.isNullType((Schema) anyOf)) { // The null type does not have a discriminator. Skip. hasNullTypeCnt++; continue; } foundDisc = recursiveGetDiscriminator((Schema) anyOf, visitedSchemas); if (foundDisc != null) { discriminatorsPropNames.add(foundDisc.getPropertyName()); hasDiscriminatorCnt++; } } if (discriminatorsPropNames.size() > 1) { 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()); disc.setMapping(foundDisc.getMapping()); return disc; } // If the scenario when anyOf has two children and one of them is the 'null' type, // there is no need for a discriminator. } } return null; } /** * This function is only used for composed schemas which have a discriminator * Process oneOf and anyOf models in a composed schema and adds them into * a list if the oneOf and anyOf models contain * the required discriminator. If they don't contain the required * discriminator or the discriminator is the wrong type then an error is * thrown * * @param composedSchemaName The String model name of the composed schema where we are setting the discriminator map * @param discPropName The String that is the discriminator propertyName in the schema * @param c The ComposedSchema that contains the discriminator and oneOf/anyOf schemas * @return the list of oneOf and anyOf MappedModel that need to be added to the discriminator map */ protected List getOneOfAnyOfDescendants(String composedSchemaName, String discPropName, Schema c) { ArrayList> listOLists = new ArrayList<>(); listOLists.add(c.getOneOf()); listOLists.add(c.getAnyOf()); List descendentSchemas = new ArrayList<>(); for (List schemaList : listOLists) { if (schemaList == null) { continue; } for (Schema sc : schemaList) { if (ModelUtils.isNullType(sc)) { continue; } String ref = sc.get$ref(); if (ref == null) { // for schemas with no ref, it is not possible to build the discriminator map // because ref is how we get the model name // we only hit this use case for a schema with inline composed schemas, and one of those // schemas also has inline composed schemas // Note: if it is only inline one level, then the inline model resolver will move it into its own // schema and make it a $ref schema in the oneOf/anyOf location LOGGER.warn( "Invalid inline schema defined in oneOf/anyOf in '{}'. Per the OpenApi spec, for this case when a composed schema defines a discriminator, the oneOf/anyOf schemas must use $ref. Change this inline definition to a $ref definition", composedSchemaName); } CodegenProperty df = discriminatorFound(composedSchemaName, sc, discPropName, new TreeSet()); String modelName = ModelUtils.getSimpleRef(ref); if (df == null || !df.isString || df.required != true) { String msgSuffix = ""; if (df == null) { msgSuffix += discPropName + " is missing from the schema, define it as required and type string"; } else { if (!df.isString) { msgSuffix += "invalid type for " + discPropName + ", set it to string"; } if (df.required != true) { String spacer = ""; if (msgSuffix.length() != 0) { spacer = ". "; } msgSuffix += spacer + "invalid optional definition of " + discPropName + ", include it in required"; } } LOGGER.warn("'{}' defines discriminator '{}', but the referenced schema '{}' is incorrect. {}", composedSchemaName, discPropName, modelName, msgSuffix); } MappedModel mm = new MappedModel(modelName, toModelName(modelName)); descendentSchemas.add(mm); Schema cs = ModelUtils.getSchema(openAPI, modelName); if (cs == null) { // cannot lookup the model based on the name LOGGER.error("Failed to lookup the schema '{}' when processing oneOf/anyOf. Please check to ensure it's defined properly.", modelName); } else { Map vendorExtensions = cs.getExtensions(); if (vendorExtensions != null && !vendorExtensions.isEmpty() && vendorExtensions.containsKey("x-discriminator-value")) { String xDiscriminatorValue = (String) vendorExtensions.get("x-discriminator-value"); mm = new MappedModel(xDiscriminatorValue, toModelName(modelName), true); descendentSchemas.add(mm); } } } } return descendentSchemas; } protected List getAllOfDescendants(String thisSchemaName) { ArrayList queue = new ArrayList(); List descendentSchemas = new ArrayList(); Map schemas = ModelUtils.getSchemas(openAPI); String currentSchemaName = thisSchemaName; Set keys = schemas.keySet(); int count = 0; // hack: avoid infinite loop on potential self-references in event our checks fail. while (100000 > count++) { for (String childName : keys) { if (childName.equals(thisSchemaName)) { continue; } Schema child = schemas.get(childName); if (ModelUtils.isComposedSchema(child)) { List parents = child.getAllOf(); if (parents != null) { for (Schema parent : parents) { String ref = parent.get$ref(); if (ref == null) { // for schemas with no ref, it is not possible to build the discriminator map // because ref is how we get the model name // we hit this use case when an allOf composed schema contains an inline schema continue; } String parentName = ModelUtils.getSimpleRef(ref); if (parentName != null && parentName.equals(currentSchemaName)) { if (queue.contains(childName) || descendentSchemas.stream().anyMatch(i -> childName.equals(i.getMappingName()))) { throw new RuntimeException("Stack overflow hit when looking for " + thisSchemaName + " an infinite loop starting and ending at " + childName + " was seen"); } queue.add(childName); break; } } } } } if (queue.size() == 0) { break; } currentSchemaName = queue.remove(0); Schema cs = schemas.get(currentSchemaName); Map vendorExtensions = cs.getExtensions(); String mappingName = Optional.ofNullable(vendorExtensions) .map(ve -> ve.get("x-discriminator-value")) .map(discriminatorValue -> (String) discriminatorValue) .orElse(currentSchemaName); MappedModel mm = new MappedModel(mappingName, toModelName(currentSchemaName), !mappingName.equals(currentSchemaName)); descendentSchemas.add(mm); } return descendentSchemas; } protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) { Discriminator sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList()); if (sourceDiscriminator == null) { return null; } CodegenDiscriminator discriminator = new CodegenDiscriminator(); String discriminatorPropertyName = sourceDiscriminator.getPropertyName(); discriminator.setPropertyName(toVarName(discriminatorPropertyName)); discriminator.setPropertyBaseName(sourceDiscriminator.getPropertyName()); discriminator.setPropertyGetter(toGetter(discriminator.getPropertyName())); if (sourceDiscriminator.getExtensions() != null) { discriminator.setVendorExtensions(sourceDiscriminator.getExtensions()); } // FIXME: there are other ways to define the type of the discriminator property (inline // for example). Handling those scenarios is too complicated for me, I'm leaving it for // the future.. String propertyType = Optional.ofNullable(schema.getProperties()) .map(p -> (Schema) p.get(discriminatorPropertyName)) .map(Schema::get$ref) .map(ModelUtils::getSimpleRef) .map(this::toModelName) .orElseGet(() -> typeMapping.get("string")); discriminator.setPropertyType(propertyType); // check to see if the discriminator property is an enum string if (schema.getProperties() != null && schema.getProperties().get(discriminatorPropertyName) instanceof StringSchema) { StringSchema s = (StringSchema) schema.getProperties().get(discriminatorPropertyName); if (s.getEnum() != null && !s.getEnum().isEmpty()) { // it's an enum string discriminator.setIsEnum(true); } } discriminator.setMapping(sourceDiscriminator.getMapping()); List uniqueDescendants = new ArrayList<>(); if (sourceDiscriminator.getMapping() != null && !sourceDiscriminator.getMapping().isEmpty()) { for (Entry e : sourceDiscriminator.getMapping().entrySet()) { String name; if (e.getValue().indexOf('/') >= 0) { name = ModelUtils.getSimpleRef(e.getValue()); if (ModelUtils.getSchema(openAPI, name) == null) { LOGGER.error("Failed to lookup the schema '{}' when processing the discriminator mapping of oneOf/anyOf. Please check to ensure it's defined properly.", name); } } else { name = e.getValue(); } uniqueDescendants.add(new MappedModel(e.getKey(), toModelName(name), true)); } } boolean legacyUseCase = (this.getLegacyDiscriminatorBehavior() && uniqueDescendants.isEmpty()); if (!this.getLegacyDiscriminatorBehavior() || legacyUseCase) { // for schemas that allOf inherit from this schema, add those descendants to this discriminator map List otherDescendants = getAllOfDescendants(schemaName); for (MappedModel otherDescendant : otherDescendants) { // add only if the mapping names are not the same and the model names are not the same boolean matched = false; for (MappedModel uniqueDescendant : uniqueDescendants) { if (uniqueDescendant.getMappingName().equals(otherDescendant.getMappingName()) || (uniqueDescendant.getModelName().equals(otherDescendant.getModelName()))) { matched = true; break; } } if (matched == false) { uniqueDescendants.add(otherDescendant); } } } // if there are composed oneOf/anyOf schemas, add them to this discriminator if (ModelUtils.isComposedSchema(schema) && !this.getLegacyDiscriminatorBehavior()) { List otherDescendants = getOneOfAnyOfDescendants(schemaName, discriminatorPropertyName, schema); for (MappedModel otherDescendant : otherDescendants) { if (!uniqueDescendants.contains(otherDescendant)) { uniqueDescendants.add(otherDescendant); } } } if (!this.getLegacyDiscriminatorBehavior()) { Collections.sort(uniqueDescendants); } discriminator.getMappedModels().addAll(uniqueDescendants); return discriminator; } /** * Handle the model for the 'additionalProperties' keyword in the OAS schema. * * @param codegenModel The codegen representation of the schema. * @param schema The input OAS schema. */ protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema schema) { addParentContainer(codegenModel, codegenModel.name, schema); } /** * Add schema's properties to "properties" and "required" list * * @param properties all properties * @param required required property only * @param schema schema in which the properties will be added to the lists * @param visitedSchemas circuit-breaker - the schemas with which the method was called before for recursive structures */ protected void addProperties(Map properties, List required, Schema schema, Set visitedSchemas) { if (schema == null) { return; } if (!visitedSchemas.add(schema)) { return; } if (ModelUtils.isComposedSchema(schema)) { if (schema.getAllOf() != null) { for (Object component : schema.getAllOf()) { addProperties(properties, required, (Schema) component, visitedSchemas); } } if (schema.getRequired() != null) { required.addAll(schema.getRequired()); } if (schema.getOneOf() != null) { for (Object component : schema.getOneOf()) { addProperties(properties, required, (Schema) component, visitedSchemas); } } if (schema.getAnyOf() != null) { for (Object component : schema.getAnyOf()) { addProperties(properties, required, (Schema) component, visitedSchemas); } } return; } if (StringUtils.isNotBlank(schema.get$ref())) { Schema interfaceSchema = ModelUtils.getReferencedSchema(this.openAPI, schema); addProperties(properties, required, interfaceSchema, visitedSchemas); return; } if (schema.getProperties() != null) { properties.putAll(schema.getProperties()); } if (schema.getRequired() != null) { required.addAll(schema.getRequired()); } } /** * Camelize the method name of the getter and setter * * @param name string to be camelized * @return Camelized string */ public String getterAndSetterCapitalize(String name) { if (name == null || name.length() == 0) { return name; } return camelize(toVarName(name)); } protected void updatePropertyForMap(CodegenProperty property, Schema p) { // throw exception if additionalProperties is false if (p.getAdditionalProperties() instanceof Boolean && Boolean.FALSE.equals(p.getAdditionalProperties())) { throw new RuntimeException("additionalProperties cannot be false in updatePropertyForMap."); } property.isContainer = true; property.containerType = "map"; property.containerTypeMapped = typeMapping.get(property.containerType); // TODO remove this hack in the future, code should use minProperties and maxProperties for object schemas property.minItems = p.getMinProperties(); property.maxItems = p.getMaxProperties(); // handle inner property Schema innerSchema = unaliasSchema(ModelUtils.getAdditionalProperties(p)); if (innerSchema == null) { LOGGER.error("Undefined map inner type for `{}`. Default to String.", p.getName()); innerSchema = new StringSchema().description("//TODO automatically added by openapi-generator due to undefined type"); p.setAdditionalProperties(innerSchema); } CodegenProperty cp = fromProperty("inner", innerSchema, false); updatePropertyForMap(property, cp); } protected void updatePropertyForObject(CodegenProperty property, Schema p) { if (ModelUtils.isFreeFormObject(p)) { // non-composed object type with no properties + additionalProperties // additionalProperties must be null, ObjectSchema, or empty Schema property.isFreeFormObject = true; if (languageSpecificPrimitives.contains(property.dataType)) { property.isPrimitiveType = true; } if (ModelUtils.isMapSchema(p)) { // an object or anyType composed schema that has additionalProperties set updatePropertyForMap(property, p); } else { // ObjectSchema with additionalProperties = null, can be nullable property.setIsMap(false); } } else if (ModelUtils.isMapSchema(p)) { // an object or anyType composed schema that has additionalProperties set updatePropertyForMap(property, p); } addVarsRequiredVarsAdditionalProps(p, property); } protected void updatePropertyForAnyType(CodegenProperty property, Schema p) { // The 'null' value is allowed when the OAS schema is 'any type'. // See https://github.com/OAI/OpenAPI-Specification/issues/1389 if (Boolean.FALSE.equals(p.getNullable())) { LOGGER.warn("Schema '{}' is any type, which includes the 'null' value. 'nullable' cannot be set to 'false'", p.getName()); } property.isNullable = property.isNullable || !(ModelUtils.isComposedSchema(p)) || p.getAllOf() == null || p.getAllOf().size() == 0; if (languageSpecificPrimitives.contains(property.dataType)) { property.isPrimitiveType = true; } if (ModelUtils.isMapSchema(p)) { // an object or anyType composed schema that has additionalProperties set // some of our code assumes that any type schema with properties defined will be a map // even though it should allow in any type and have map constraints for properties updatePropertyForMap(property, p); } addVarsRequiredVarsAdditionalProps(p, property); } protected void updatePropertyForString(CodegenProperty property, Schema p) { if (ModelUtils.isByteArraySchema(p)) { property.setIsString(false); property.isByteArray = true; } else if (ModelUtils.isBinarySchema(p)) { property.isBinary = true; property.isFile = true; // file = binary in OAS3 } else if (ModelUtils.isUUIDSchema(p)) { property.isUuid = true; } else if (ModelUtils.isURISchema(p)) { property.isUri = true; } else if (ModelUtils.isEmailSchema(p)) { property.isEmail = true; } else if (ModelUtils.isPasswordSchema(p)) { property.isPassword = true; } else if (ModelUtils.isDateSchema(p)) { // date format property.setIsString(false); // for backward compatibility with 2.x property.isDate = true; } else if (ModelUtils.isDateTimeSchema(p)) { // date-time format property.setIsString(false); // for backward compatibility with 2.x property.isDateTime = true; } else if (ModelUtils.isDecimalSchema(p)) { // type: string, format: number property.isDecimal = true; property.setIsString(false); } property.pattern = toRegularExpression(p.getPattern()); } protected void updatePropertyForNumber(CodegenProperty property, Schema p) { property.isNumeric = Boolean.TRUE; if (ModelUtils.isFloatSchema(p)) { // float property.isFloat = Boolean.TRUE; } else if (ModelUtils.isDoubleSchema(p)) { // double property.isDouble = Boolean.TRUE; } } protected void updatePropertyForInteger(CodegenProperty property, Schema p) { property.isNumeric = Boolean.TRUE; if (ModelUtils.isLongSchema(p)) { // int64/long format property.isLong = Boolean.TRUE; } else { property.isInteger = Boolean.TRUE; // older use case, int32 and unbounded int if (ModelUtils.isShortSchema(p)) { // int32 property.setIsShort(Boolean.TRUE); } } } /** * TODO remove this in 7.0.0 as a breaking change * This method was kept when required was added to the fromProperty signature * to ensure that the change was non-breaking * * @param name name of the property * @param p OAS property schema * @param required true if the property is required in the next higher object schema, false otherwise * @return Codegen Property object */ public CodegenProperty fromProperty(String name, Schema p, boolean required) { return fromProperty(name, p, required, false); } /** * TODO remove this in 7.0.0 as a breaking change * This method was kept when required was added to the fromProperty signature * to ensure that the change was non-breaking * * @param name name of the property * @param p OAS property schema * @return Codegen Property object */ public CodegenProperty fromProperty(String name, Schema p) { return fromProperty(name, p, false, false); } /** * Convert OAS Property object to Codegen Property object. *

* The return value is cached. An internal cache is looked up to determine * if the CodegenProperty return value has already been instantiated for * the (String name, Schema p) arguments. * Any subsequent processing of the CodegenModel return value must be idempotent * for a given (String name, Schema schema). * * @param name name of the property * @param p OAS property schema * @param required true if the property is required in the next higher object schema, false otherwise * @param schemaIsFromAdditionalProperties true if the property is a required property defined by additional properties schema * If this is the actual additionalProperties schema not defining a required property, then * the value should be false * @return Codegen Property object */ public CodegenProperty fromProperty(String name, Schema p, boolean required, boolean schemaIsFromAdditionalProperties) { if (p == null) { LOGGER.error("Undefined property/schema for `{}`. Default to type:string.", name); return null; } LOGGER.debug("debugging fromProperty for {} : {}", name, p); NamedSchema ns = new NamedSchema(name, p, required, schemaIsFromAdditionalProperties); CodegenProperty cpc = schemaCodegenPropertyCache.get(ns); if (cpc != null) { LOGGER.debug("Cached fromProperty for {} : {} required={}", name, p.getName(), required); return cpc; } 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(p) && p.getAllOf().size() == 1) { if (p.getAllOf().get(0) instanceof Schema) { original = p; p = (Schema) p.getAllOf().get(0); } else { LOGGER.error("Unknown type in allOf schema. Please report the issue via openapi-generator's Github issue tracker."); } } CodegenProperty property = CodegenModelFactory.newInstance(CodegenModelType.PROPERTY); if (p.equals(trueSchema)) { property.setIsBooleanSchemaTrue(true); } else if (p.equals(falseSchema)) { property.setIsBooleanSchemaFalse(true); } // unalias schema p = unaliasSchema(p); property.setSchemaIsFromAdditionalProperties(schemaIsFromAdditionalProperties); property.required = required; ModelUtils.syncValidationProperties(p, property); property.setFormat(p.getFormat()); property.name = toVarName(name); property.baseName = name; if (p.getType() == null) { property.openApiType = getSchemaType(p); } else { property.openApiType = p.getType(); } property.nameInCamelCase = camelize(property.name); property.nameInSnakeCase = CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, property.nameInCamelCase); property.description = escapeText(p.getDescription()); property.unescapedDescription = p.getDescription(); property.title = p.getTitle(); property.getter = toGetter(name); property.setter = toSetter(name); // put toExampleValue in a try-catch block to log the error as example values are not critical try { property.example = toExampleValue(p); } catch (Exception e) { LOGGER.error("Error in generating `example` for the property {}. Default to ERROR_TO_EXAMPLE_VALUE. Enable debugging for more info.", property.baseName); LOGGER.debug("Exception from toExampleValue: {}", e.getMessage()); property.example = "ERROR_TO_EXAMPLE_VALUE"; } property.jsonSchema = Json.pretty(Json.mapper().convertValue(p, TreeMap.class)); if (p.getDeprecated() != null) { property.deprecated = p.getDeprecated(); } else if (p.get$ref() != null) { // Since $ref should be replaced with the model it refers // to, $ref'ing a model with 'deprecated' set should cause // the property to reflect the model's 'deprecated' value. String ref = ModelUtils.getSimpleRef(p.get$ref()); if (ref != null) { Schema referencedSchema = ModelUtils.getSchemas(this.openAPI).get(ref); if (referencedSchema != null && referencedSchema.getDeprecated() != null) { property.deprecated = referencedSchema.getDeprecated(); } } } if (p.getReadOnly() != null) { property.isReadOnly = p.getReadOnly(); } if (p.getWriteOnly() != null) { property.isWriteOnly = p.getWriteOnly(); } if (p.getNullable() != null) { property.isNullable = p.getNullable(); } if (p.getExtensions() != null && !p.getExtensions().isEmpty()) { property.getVendorExtensions().putAll(p.getExtensions()); } else if (p.get$ref() != null) { Schema referencedSchema = ModelUtils.getReferencedSchema(this.openAPI, p); if (referencedSchema.getExtensions() != null && !referencedSchema.getExtensions().isEmpty()) { property.getVendorExtensions().putAll(referencedSchema.getExtensions()); } } //Inline enum case: if (p.getEnum() != null && !p.getEnum().isEmpty()) { List _enum = p.getEnum(); property._enum = new ArrayList<>(); for (Object i : _enum) { property._enum.add(String.valueOf(i)); } property.isEnum = true; property.isInnerEnum = true; Map allowableValues = new HashMap<>(); allowableValues.put("values", _enum); if (allowableValues.size() > 0) { property.allowableValues = allowableValues; } } Schema referencedSchema = ModelUtils.getReferencedSchema(this.openAPI, p); //Referenced enum case: if (referencedSchema != p && referencedSchema.getEnum() != null && !referencedSchema.getEnum().isEmpty()) { List _enum = referencedSchema.getEnum(); property.isEnumRef = true; Map allowableValues = new HashMap<>(); allowableValues.put("values", _enum); if (allowableValues.size() > 0) { property.allowableValues = allowableValues; } } // set isNullable using nullable or x-nullable in the schema if (referencedSchema.getNullable() != null) { property.isNullable = referencedSchema.getNullable(); } else if (referencedSchema.getExtensions() != null && referencedSchema.getExtensions().containsKey("x-nullable")) { property.isNullable = (Boolean) referencedSchema.getExtensions().get("x-nullable"); } final XML referencedSchemaXml = referencedSchema.getXml(); if (referencedSchemaXml != null) { property.xmlName = referencedSchemaXml.getName(); property.xmlNamespace = referencedSchemaXml.getNamespace(); property.xmlPrefix = referencedSchemaXml.getPrefix(); if (referencedSchemaXml.getAttribute() != null) { property.isXmlAttribute = referencedSchemaXml.getAttribute(); } if (referencedSchemaXml.getWrapped() != null) { property.isXmlWrapped = referencedSchemaXml.getWrapped(); } } if (p.getXml() != null) { if (p.getXml().getAttribute() != null) { property.isXmlAttribute = p.getXml().getAttribute(); } if (p.getXml().getWrapped() != null) { property.isXmlWrapped = p.getXml().getWrapped(); } property.xmlPrefix = p.getXml().getPrefix(); property.xmlName = p.getXml().getName(); property.xmlNamespace = p.getXml().getNamespace(); } property.dataType = getTypeDeclaration(p); property.dataFormat = p.getFormat(); property.baseType = getSchemaType(p); // this can cause issues for clients which don't support enums if (property.isEnum) { property.datatypeWithEnum = toEnumName(property); property.enumName = toEnumName(property); } else { property.datatypeWithEnum = property.dataType; } property.setTypeProperties(p); property.setComposedSchemas(getComposedSchemas(p)); if (ModelUtils.isIntegerSchema(p)) { // integer type updatePropertyForInteger(property, p); } else if (ModelUtils.isBooleanSchema(p)) { // boolean type property.getter = toBooleanGetter(name); } else if (ModelUtils.isFileSchema(p) && !ModelUtils.isStringSchema(p)) { // swagger v2 only, type file property.isFile = true; } else if (ModelUtils.isStringSchema(p)) { updatePropertyForString(property, p); } else if (ModelUtils.isNumberSchema(p)) { updatePropertyForNumber(property, p); } else if (ModelUtils.isArraySchema(p)) { // default to string if inner item is undefined 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.baseType = getSchemaType(p); // handle inner property String itemName = getItemsName(p, name); ArraySchema arraySchema = (ArraySchema) p; Schema innerSchema = unaliasSchema(getSchemaItems(arraySchema)); CodegenProperty cp = fromProperty(itemName, innerSchema, false); updatePropertyForArray(property, cp); } else if (ModelUtils.isTypeObjectSchema(p)) { updatePropertyForObject(property, p); } else if (ModelUtils.isAnyType(p)) { updatePropertyForAnyType(property, p); } else if (!ModelUtils.isNullType(p)) { // referenced model } if (p.get$ref() != null) { property.setRef(p.get$ref()); } boolean isAnyTypeWithNothingElseSet = (ModelUtils.isAnyType(p) && (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) && !isAnyTypeWithNothingElseSet) { /* schemas that are not Array, not ModelUtils.isMapSchema, not isFreeFormObject, not AnyType with nothing else set * so primitive schemas int, str, number, referenced schemas, AnyType schemas with properties, enums, or composition */ String type = getSchemaType(p); setNonArrayMapProperty(property, type); property.isModel = (ModelUtils.isComposedSchema(referencedSchema) || ModelUtils.isObjectSchema(referencedSchema)) && ModelUtils.isModel(referencedSchema); } // restore original schema with default value, nullable, readonly etc if (original != null) { p = original; // evaluate common attributes if defined in the top level if (p.getNullable() != null) { property.isNullable = p.getNullable(); } else if (p.getExtensions() != null && p.getExtensions().containsKey("x-nullable")) { property.isNullable = (Boolean) p.getExtensions().get("x-nullable"); } if (p.getReadOnly() != null) { property.isReadOnly = p.getReadOnly(); } if (p.getWriteOnly() != null) { property.isWriteOnly = p.getWriteOnly(); } if (original.getExtensions() != null) { property.getVendorExtensions().putAll(original.getExtensions()); } if (original.getDeprecated() != null) { property.deprecated = p.getDeprecated(); } } // set the default value property.defaultValue = toDefaultValue(property, p); property.defaultValueWithParam = toDefaultValueWithParam(name, p); LOGGER.debug("debugging from property return: {}", property); schemaCodegenPropertyCache.put(ns, property); return property; } /** * Update property for array(list) container * * @param property Codegen property * @param innerProperty Codegen inner property of map or list */ protected void updatePropertyForArray(CodegenProperty property, CodegenProperty innerProperty) { if (innerProperty == null) { if (LOGGER.isWarnEnabled()) { LOGGER.warn("skipping invalid array property {}", Json.pretty(property)); } return; } property.dataFormat = innerProperty.dataFormat; if (!languageSpecificPrimitives.contains(innerProperty.baseType)) { property.complexType = innerProperty.baseType; } else { property.isPrimitiveType = true; } property.items = innerProperty; property.mostInnerItems = getMostInnerItems(innerProperty); // inner item is Enum if (isPropertyInnerMostEnum(property)) { // isEnum is set to true when the type is an enum // or the inner type of an array/map is an enum property.isEnum = true; property.isInnerEnum = true; // update datatypeWithEnum and default value for array // e.g. List => List updateDataTypeWithEnumForArray(property); // set allowable values to enum values (including array/map of enum) property.allowableValues = getInnerEnumAllowableValues(property); } } /** * Update property for map container * * @param property Codegen property * @param innerProperty Codegen inner property of map or list */ protected void updatePropertyForMap(CodegenProperty property, CodegenProperty innerProperty) { if (innerProperty == null) { if (LOGGER.isWarnEnabled()) { LOGGER.warn("skipping invalid map property {}", Json.pretty(property)); } return; } if (!languageSpecificPrimitives.contains(innerProperty.baseType)) { property.complexType = innerProperty.baseType; } else { property.isPrimitiveType = true; } // TODO fix this, map should not be assigning properties to items property.items = innerProperty; property.mostInnerItems = getMostInnerItems(innerProperty); property.dataFormat = innerProperty.dataFormat; // inner item is Enum if (isPropertyInnerMostEnum(property)) { // isEnum is set to true when the type is an enum // or the inner type of an array/map is an enum property.isEnum = true; property.isInnerEnum = true; // update datatypeWithEnum and default value for map // e.g. Dictionary => Dictionary updateDataTypeWithEnumForMap(property); // set allowable values to enum values (including array/map of enum) property.allowableValues = getInnerEnumAllowableValues(property); } } /** * Update property for map container * * @param property Codegen property * @return True if the inner most type is enum */ protected Boolean isPropertyInnerMostEnum(CodegenProperty property) { CodegenProperty currentProperty = getMostInnerItems(property); return currentProperty != null && currentProperty.isEnum; } protected CodegenProperty getMostInnerItems(CodegenProperty property) { CodegenProperty currentProperty = property; while (currentProperty != null && (Boolean.TRUE.equals(currentProperty.isMap) || Boolean.TRUE.equals(currentProperty.isArray)) && currentProperty.items != null) { currentProperty = currentProperty.items; } return currentProperty; } protected Map getInnerEnumAllowableValues(CodegenProperty property) { CodegenProperty currentProperty = getMostInnerItems(property); return currentProperty == null ? new HashMap<>() : currentProperty.allowableValues; } /** * Update datatypeWithEnum for array container * * @param property Codegen property */ protected void updateDataTypeWithEnumForArray(CodegenProperty property) { CodegenProperty baseItem = property.items; while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap) || Boolean.TRUE.equals(baseItem.isArray))) { baseItem = baseItem.items; } if (baseItem != null) { // set both datatype and datetypeWithEnum as only the inner type is enum property.datatypeWithEnum = property.datatypeWithEnum.replace(baseItem.baseType, toEnumName(baseItem)); // naming the enum with respect to the language enum naming convention // e.g. remove [], {} from array/map of enum property.enumName = toEnumName(property); // set default value for variable with inner enum if (property.defaultValue != null) { property.defaultValue = property.defaultValue.replace(baseItem.baseType, toEnumName(baseItem)); } updateCodegenPropertyEnum(property); } } /** * Update datatypeWithEnum for map container * * @param property Codegen property */ protected void updateDataTypeWithEnumForMap(CodegenProperty property) { CodegenProperty baseItem = property.items; while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap) || Boolean.TRUE.equals(baseItem.isArray))) { baseItem = baseItem.items; } if (baseItem != null) { // set both datatype and datetypeWithEnum as only the inner type is enum property.datatypeWithEnum = property.datatypeWithEnum.replace(", " + baseItem.baseType, ", " + toEnumName(baseItem)); // naming the enum with respect to the language enum naming convention // e.g. remove [], {} from array/map of enum property.enumName = toEnumName(property); // set default value for variable with inner enum if (property.defaultValue != null) { property.defaultValue = property.defaultValue.replace(", " + property.items.baseType, ", " + toEnumName(property.items)); } updateCodegenPropertyEnum(property); } } protected void setNonArrayMapProperty(CodegenProperty property, String type) { property.isContainer = false; if (languageSpecificPrimitives().contains(type)) { property.isPrimitiveType = true; } else { property.complexType = property.baseType; property.isModel = true; } } /** * Override with any special handling of response codes * * @param responses OAS Operation's responses * @return default method response or null if not found */ protected ApiResponse findMethodResponse(ApiResponses responses) { String code = null; for (String responseCode : responses.keySet()) { if (responseCode.startsWith("2") || responseCode.equals("default")) { if (code == null || code.compareTo(responseCode) > 0) { code = responseCode; } } } if (code == null) { return null; } return responses.get(code); } /** * Set op's returnBaseType, returnType, examples etc. * * @param operation endpoint Operation * @param schemas a map of the schemas in the openapi spec * @param op endpoint CodegenOperation * @param methodResponse the default ApiResponse for the endpoint */ protected void handleMethodResponse(Operation operation, Map schemas, CodegenOperation op, ApiResponse methodResponse) { handleMethodResponse(operation, schemas, op, methodResponse, Collections.emptyMap()); } /** * Set op's returnBaseType, returnType, examples etc. * * @param operation endpoint Operation * @param schemas a map of the schemas in the openapi spec * @param op endpoint CodegenOperation * @param methodResponse the default ApiResponse for the endpoint * @param schemaMappings mappings of external types to be omitted by unaliasing */ protected void handleMethodResponse(Operation operation, Map schemas, CodegenOperation op, ApiResponse methodResponse, Map schemaMappings) { Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, methodResponse)); if (responseSchema != null) { CodegenProperty cm = fromProperty("response", responseSchema, false); if (ModelUtils.isArraySchema(responseSchema)) { ArraySchema as = (ArraySchema) responseSchema; CodegenProperty innerProperty = fromProperty("response", getSchemaItems(as), false); op.returnBaseType = innerProperty.baseType; } else if (ModelUtils.isMapSchema(responseSchema)) { CodegenProperty innerProperty = fromProperty("response", ModelUtils.getAdditionalProperties(responseSchema), false); op.returnBaseType = innerProperty.baseType; } else { if (cm.complexType != null) { op.returnBaseType = cm.complexType; } else { op.returnBaseType = cm.baseType; } } // check skipOperationExample, which can be set to true to avoid out of memory errors for large spec if (!isSkipOperationExample()) { // generate examples String exampleStatusCode = "200"; for (String key : operation.getResponses().keySet()) { if (operation.getResponses().get(key) == methodResponse && !key.equals("default")) { exampleStatusCode = key; } } op.examples = new ExampleGenerator(schemas, this.openAPI).generateFromResponseSchema(exampleStatusCode, responseSchema, getProducesInfo(this.openAPI, operation)); } op.defaultResponse = toDefaultValue(responseSchema); op.returnType = cm.dataType; op.returnFormat = cm.dataFormat; op.hasReference = schemas != null && schemas.containsKey(op.returnBaseType); // lookup discriminator Schema schema = null; if (schemas != null) { schema = schemas.get(op.returnBaseType); } if (schema != null) { CodegenModel cmod = fromModel(op.returnBaseType, schema); op.discriminator = cmod.discriminator; } if (cm.isContainer) { op.returnContainer = cm.containerType; if ("map".equals(cm.containerType)) { op.isMap = true; } else if ("list".equalsIgnoreCase(cm.containerType)) { op.isArray = true; } else if ("array".equalsIgnoreCase(cm.containerType)) { op.isArray = true; } else if ("set".equalsIgnoreCase(cm.containerType)) { op.isArray = true; } } else { op.returnSimpleType = true; } if (languageSpecificPrimitives().contains(op.returnBaseType) || op.returnBaseType == null) { op.returnTypeIsPrimitive = true; } op.returnProperty = cm; } addHeaders(methodResponse, op.responseHeaders); } /** * Convert OAS Operation object to Codegen Operation object * * @param httpMethod HTTP method * @param operation OAS operation object * @param path the path of the operation * @param servers list of servers * @return Codegen Operation object */ @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { LOGGER.debug("fromOperation => operation: {}", operation); if (operation == null) throw new RuntimeException("operation cannot be null in fromOperation"); Map schemas = ModelUtils.getSchemas(this.openAPI); CodegenOperation op = CodegenModelFactory.newInstance(CodegenModelType.OPERATION); Set imports = new HashSet<>(); if (operation.getExtensions() != null && !operation.getExtensions().isEmpty()) { op.vendorExtensions.putAll(operation.getExtensions()); Object isCallbackRequest = op.vendorExtensions.remove("x-callback-request"); op.isCallbackRequest = Boolean.TRUE.equals(isCallbackRequest); } // servers setting if (operation.getServers() != null && !operation.getServers().isEmpty()) { // use operation-level servers first if defined op.servers = fromServers(operation.getServers()); } else if (servers != null && !servers.isEmpty()) { // use path-level servers op.servers = fromServers(servers); } // store the original operationId for plug-in op.operationIdOriginal = operation.getOperationId(); String operationId = getOrGenerateOperationId(operation, path, httpMethod); // remove prefix in operationId if (removeOperationIdPrefix) { // The prefix is everything before the removeOperationIdPrefixCount occurrence of removeOperationIdPrefixDelimiter String[] components = operationId.split("[" + removeOperationIdPrefixDelimiter + "]"); if (components.length > 1) { // If removeOperationIdPrefixCount is -1 or bigger that the number of occurrences, uses the last one int component_number = removeOperationIdPrefixCount == -1 ? components.length - 1 : removeOperationIdPrefixCount; component_number = Math.min(component_number, components.length - 1); // Reconstruct the operationId from its split elements and the delimiter operationId = String.join(removeOperationIdPrefixDelimiter, Arrays.copyOfRange(components, component_number, components.length)); } } operationId = removeNonNameElementToCamelCase(operationId); if (isStrictSpecBehavior() && !path.startsWith("/")) { // modifies an operation.path to strictly conform to OpenAPI Spec op.path = "/" + path; } else { op.path = path; } op.operationId = toOperationId(operationId); op.summary = escapeText(operation.getSummary()); op.unescapedNotes = operation.getDescription(); op.notes = escapeText(operation.getDescription()); op.hasConsumes = false; op.hasProduces = false; if (operation.getDeprecated() != null) { op.isDeprecated = operation.getDeprecated(); } addConsumesInfo(operation, op); if (operation.getResponses() != null && !operation.getResponses().isEmpty()) { ApiResponse methodResponse = findMethodResponse(operation.getResponses()); for (Map.Entry operationGetResponsesEntry : operation.getResponses().entrySet()) { String key = operationGetResponsesEntry.getKey(); ApiResponse response = operationGetResponsesEntry.getValue(); addProducesInfo(response, op); CodegenResponse r = fromResponse(key, response); Map headers = response.getHeaders(); if (headers != null) { List responseHeaders = new ArrayList<>(); for (Entry entry : headers.entrySet()) { String headerName = entry.getKey(); Header header = ModelUtils.getReferencedHeader(this.openAPI, entry.getValue()); CodegenParameter responseHeader = headerToCodegenParameter(header, headerName, imports, String.format(Locale.ROOT, "%sResponseParameter", r.code)); responseHeaders.add(responseHeader); } r.setResponseHeaders(responseHeaders); } String mediaTypeSchemaSuffix = String.format(Locale.ROOT, "%sResponseBody", r.code); r.setContent(getContent(response.getContent(), imports, mediaTypeSchemaSuffix)); if (r.baseType != null && !defaultIncludes.contains(r.baseType) && !languageSpecificPrimitives.contains(r.baseType)) { imports.add(r.baseType); } if ("set".equals(r.containerType) && typeMapping.containsKey(r.containerType)) { op.uniqueItems = true; imports.add(typeMapping.get(r.containerType)); } op.responses.add(r); if (Boolean.TRUE.equals(r.isBinary) && Boolean.TRUE.equals(r.is2xx) && Boolean.FALSE.equals(op.isResponseBinary)) { op.isResponseBinary = Boolean.TRUE; } if (Boolean.TRUE.equals(r.isFile) && Boolean.TRUE.equals(r.is2xx) && Boolean.FALSE.equals(op.isResponseFile)) { op.isResponseFile = Boolean.TRUE; } if (Boolean.TRUE.equals(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)) { 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.isResponseOptional = Boolean.TRUE; } op.responses.sort((a, b) -> { int aScore = a.isWildcard() ? 2 : a.isRange() ? 1 : 0; int bScore = b.isWildcard() ? 2 : b.isRange() ? 1 : 0; return Integer.compare(aScore, bScore); }); if (methodResponse != null) { handleMethodResponse(operation, schemas, op, methodResponse, importMapping); } } if (operation.getCallbacks() != null && !operation.getCallbacks().isEmpty()) { operation.getCallbacks().forEach((name, callback) -> { CodegenCallback c = fromCallback(name, callback, servers); op.callbacks.add(c); }); } List parameters = operation.getParameters(); List allParams = new ArrayList<>(); List bodyParams = new ArrayList<>(); List pathParams = new ArrayList<>(); List queryParams = new ArrayList<>(); List headerParams = new ArrayList<>(); List cookieParams = new ArrayList<>(); List formParams = new ArrayList<>(); List requiredParams = new ArrayList<>(); List optionalParams = new ArrayList<>(); List requiredAndNotNullableParams = new ArrayList<>(); List notNullableParams = new ArrayList<>(); CodegenParameter bodyParam = null; RequestBody requestBody = operation.getRequestBody(); if (requestBody != null) { String contentType = getContentType(requestBody); if (contentType != null) { contentType = contentType.toLowerCase(Locale.ROOT); } if (contentType != null && (contentType.startsWith("application/x-www-form-urlencoded") || contentType.startsWith("multipart"))) { // process form parameters formParams = fromRequestBodyToFormParameters(requestBody, imports); op.isMultipart = contentType.startsWith("multipart"); for (CodegenParameter cp : formParams) { setParameterEncodingValues(cp, requestBody.getContent().get(contentType)); postProcessParameter(cp); } // add form parameters to the beginning of all parameter list if (prependFormOrBodyParameters) { for (CodegenParameter cp : formParams) { allParams.add(cp.copy()); } } } else { // process body parameter requestBody = ModelUtils.getReferencedRequestBody(this.openAPI, requestBody); String bodyParameterName = ""; if (op.vendorExtensions != null && op.vendorExtensions.containsKey("x-codegen-request-body-name")) { bodyParameterName = (String) op.vendorExtensions.get("x-codegen-request-body-name"); } bodyParam = fromRequestBody(requestBody, imports, bodyParameterName); if (bodyParam != null) { bodyParam.description = escapeText(requestBody.getDescription()); postProcessParameter(bodyParam); bodyParams.add(bodyParam); if (prependFormOrBodyParameters) { allParams.add(bodyParam); } // add example if (schemas != null && !isSkipOperationExample()) { op.requestBodyExamples = new ExampleGenerator(schemas, this.openAPI).generate(null, new ArrayList<>(getConsumesInfo(this.openAPI, operation)), bodyParam.baseType); } } } } if (parameters != null) { for (Parameter param : parameters) { param = ModelUtils.getReferencedParameter(this.openAPI, param); CodegenParameter p = fromParameter(param, imports); p.setContent(getContent(param.getContent(), imports, "RequestParameter" + toModelName(param.getName()))); // ensure unique params if (ensureUniqueParams) { while (!isParameterNameUnique(p, allParams)) { p.paramName = generateNextName(p.paramName); } } allParams.add(p); if (param instanceof QueryParameter || "query".equalsIgnoreCase(param.getIn())) { queryParams.add(p.copy()); } else if (param instanceof PathParameter || "path".equalsIgnoreCase(param.getIn())) { pathParams.add(p.copy()); } else if (param instanceof HeaderParameter || "header".equalsIgnoreCase(param.getIn())) { headerParams.add(p.copy()); } else if (param instanceof CookieParameter || "cookie".equalsIgnoreCase(param.getIn())) { cookieParams.add(p.copy()); } else { LOGGER.warn("Unknown parameter type {} for {}", p.baseType, p.baseName); } } } // add form/body parameter (if any) to the end of all parameter list if (!prependFormOrBodyParameters) { for (CodegenParameter cp : formParams) { if (ensureUniqueParams) { while (!isParameterNameUnique(cp, allParams)) { cp.paramName = generateNextName(cp.paramName); } } allParams.add(cp.copy()); } for (CodegenParameter cp : bodyParams) { if (ensureUniqueParams) { while (!isParameterNameUnique(cp, allParams)) { cp.paramName = generateNextName(cp.paramName); } } allParams.add(cp.copy()); } } // create optional, required parameters for (CodegenParameter cp : allParams) { if (cp.required) { //required parameters requiredParams.add(cp.copy()); } else { // optional parameters optionalParams.add(cp.copy()); op.hasOptionalParams = true; } if (cp.requiredAndNotNullable()) { requiredAndNotNullableParams.add(cp.copy()); } if (!cp.isNullable) { notNullableParams.add(cp.copy()); } } // add imports to operation import tag for (String i : imports) { if (needToImport(i)) { op.imports.add(i); } } op.bodyParam = bodyParam; op.httpMethod = httpMethod.toUpperCase(Locale.ROOT); // move "required" parameters in front of "optional" parameters if (sortParamsByRequiredFlag) { Collections.sort(allParams, new Comparator() { @Override public int compare(CodegenParameter one, CodegenParameter another) { if (one.required == another.required) return 0; else if (one.required) return -1; else return 1; } }); } op.allParams = allParams; op.bodyParams = bodyParams; op.pathParams = pathParams; op.queryParams = queryParams; op.headerParams = headerParams; op.cookieParams = cookieParams; op.formParams = formParams; op.requiredParams = requiredParams; op.optionalParams = optionalParams; op.requiredAndNotNullableParams = requiredAndNotNullableParams; op.notNullableParams = notNullableParams; op.externalDocs = operation.getExternalDocs(); // legacy support op.nickname = op.operationId; if (op.allParams.size() > 0) { op.hasParams = true; } op.hasRequiredParams = op.requiredParams.size() > 0; // set Restful Flag op.isRestfulShow = op.isRestfulShow(); op.isRestfulIndex = op.isRestfulIndex(); op.isRestfulCreate = op.isRestfulCreate(); op.isRestfulUpdate = op.isRestfulUpdate(); op.isRestfulDestroy = op.isRestfulDestroy(); op.isRestful = op.isRestful(); return op; } public boolean isParameterNameUnique(CodegenParameter p, List parameters) { for (CodegenParameter parameter : parameters) { if (System.identityHashCode(p) == System.identityHashCode(parameter)) { continue; // skip itself } if (p.paramName.equals(parameter.paramName)) { return false; } } return true; } /** * Convert OAS Response object to Codegen Response object * * @param responseCode HTTP response code * @param response OAS Response object * @return Codegen Response object */ public CodegenResponse fromResponse(String responseCode, ApiResponse response) { CodegenResponse r = CodegenModelFactory.newInstance(CodegenModelType.RESPONSE); if ("default".equals(responseCode) || "defaultResponse".equals(responseCode)) { r.code = "0"; r.isDefault = true; } else { 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); } } Schema responseSchema; if (this.openAPI != null && this.openAPI.getComponents() != null) { responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, response)); } else { // no model/alias defined responseSchema = ModelUtils.getSchemaFromResponse(openAPI, response); } r.schema = responseSchema; r.message = escapeText(response.getDescription()); // TODO need to revise and test examples in responses // ApiResponse does not support examples at the moment //r.examples = toExamples(response.getExamples()); r.jsonSchema = Json.pretty(response); if (response.getExtensions() != null && !response.getExtensions().isEmpty()) { r.vendorExtensions.putAll(response.getExtensions()); } addHeaders(response, r.headers); r.hasHeaders = !r.headers.isEmpty(); if (r.schema == null) { r.primitiveType = true; r.simpleType = true; return r; } ModelUtils.syncValidationProperties(responseSchema, r); if (responseSchema.getPattern() != null) { r.setPattern(toRegularExpression(responseSchema.getPattern())); } CodegenProperty cp = fromProperty("response", responseSchema, false); r.dataType = getTypeDeclaration(responseSchema); r.returnProperty = cp; if (!ModelUtils.isArraySchema(responseSchema)) { if (cp.complexType != null) { if (cp.items != null) { r.baseType = cp.items.complexType; } else { r.baseType = cp.complexType; } r.isModel = true; } else { r.baseType = cp.baseType; } } r.setTypeProperties(responseSchema); r.setComposedSchemas(getComposedSchemas(responseSchema)); if (ModelUtils.isArraySchema(responseSchema)) { r.simpleType = false; r.isArray = true; r.containerType = cp.containerType; r.containerTypeMapped = typeMapping.get(cp.containerType); ArraySchema as = (ArraySchema) responseSchema; CodegenProperty items = fromProperty("response", getSchemaItems(as), false); r.setItems(items); CodegenProperty innerCp = items; while (innerCp != null) { r.baseType = innerCp.baseType; innerCp = innerCp.items; } } else if (ModelUtils.isFileSchema(responseSchema) && !ModelUtils.isStringSchema(responseSchema)) { // swagger v2 only, type file r.isFile = true; } else if (ModelUtils.isStringSchema(responseSchema)) { if (ModelUtils.isEmailSchema(responseSchema)) { r.isEmail = true; } else if (ModelUtils.isPasswordSchema(responseSchema)) { r.isPassword = true; } else if (ModelUtils.isUUIDSchema(responseSchema)) { r.isUuid = true; } else if (ModelUtils.isByteArraySchema(responseSchema)) { r.setIsString(false); r.isByteArray = true; } else if (ModelUtils.isBinarySchema(responseSchema)) { r.isFile = true; // file = binary in OAS3 r.isBinary = true; } else if (ModelUtils.isDateSchema(responseSchema)) { r.setIsString(false); // for backward compatibility with 2.x r.isDate = true; } else if (ModelUtils.isDateTimeSchema(responseSchema)) { r.setIsString(false); // for backward compatibility with 2.x r.isDateTime = true; } else if (ModelUtils.isDecimalSchema(responseSchema)) { // type: string, format: number r.isDecimal = true; r.setIsString(false); r.isNumeric = true; } } else if (ModelUtils.isIntegerSchema(responseSchema)) { // integer type r.isNumeric = Boolean.TRUE; if (ModelUtils.isLongSchema(responseSchema)) { // int64/long format r.isLong = Boolean.TRUE; } else { r.isInteger = Boolean.TRUE; // older use case, int32 and unbounded int if (ModelUtils.isShortSchema(responseSchema)) { // int32 r.setIsShort(Boolean.TRUE); } } } else if (ModelUtils.isNumberSchema(responseSchema)) { r.isNumeric = Boolean.TRUE; if (ModelUtils.isFloatSchema(responseSchema)) { // float r.isFloat = Boolean.TRUE; } else if (ModelUtils.isDoubleSchema(responseSchema)) { // double r.isDouble = Boolean.TRUE; } } else if (ModelUtils.isTypeObjectSchema(responseSchema)) { if (ModelUtils.isFreeFormObject(responseSchema)) { r.isFreeFormObject = true; } else { r.isModel = true; } r.simpleType = false; r.containerType = cp.containerType; r.containerTypeMapped = cp.containerTypeMapped; addVarsRequiredVarsAdditionalProps(responseSchema, r); } else if (ModelUtils.isAnyType(responseSchema)) { addVarsRequiredVarsAdditionalProps(responseSchema, r); } else if (!ModelUtils.isBooleanSchema(responseSchema)) { // referenced schemas LOGGER.debug("Property type is not primitive: {}", cp.dataType); } r.primitiveType = (r.baseType == null || languageSpecificPrimitives().contains(r.baseType)); if (r.baseType == null) { r.isMap = false; r.isArray = false; r.primitiveType = true; r.simpleType = true; } postProcessResponseWithProperty(r, cp); return r; } /** * Convert OAS Callback object to Codegen Callback object * * @param name callback name * @param callback OAS Callback object * @param servers list of servers * @return Codegen Response object */ public CodegenCallback fromCallback(String name, Callback callback, List servers) { CodegenCallback c = new CodegenCallback(); c.name = name; if (callback.getExtensions() != null && !callback.getExtensions().isEmpty()) { c.vendorExtensions.putAll(callback.getExtensions()); } callback.forEach((expression, pi) -> { CodegenCallback.Url u = new CodegenCallback.Url(); u.expression = expression; if (pi.getExtensions() != null && !pi.getExtensions().isEmpty()) { u.vendorExtensions.putAll(pi.getExtensions()); } Stream.of( Pair.of("get", pi.getGet()), Pair.of("head", pi.getHead()), Pair.of("put", pi.getPut()), Pair.of("post", pi.getPost()), Pair.of("delete", pi.getDelete()), Pair.of("patch", pi.getPatch()), Pair.of("options", pi.getOptions())) .filter(p -> p.getValue() != null) .forEach(p -> { String method = p.getKey(); Operation op = p.getValue(); boolean genId = op.getOperationId() == null; if (genId) { op.setOperationId(getOrGenerateOperationId(op, c.name + "_" + expression.replaceAll("\\{\\$.*}", ""), method)); } if (op.getExtensions() == null) { op.setExtensions(new HashMap<>()); } // This extension will be removed later by `fromOperation()` as it is only needed here to // distinguish between normal operations and callback requests op.getExtensions().put("x-callback-request", true); CodegenOperation co = fromOperation(expression, method, op, servers); if (genId) { co.operationIdOriginal = null; // legacy (see `fromOperation()`) co.nickname = co.operationId; } u.requests.add(co); }); c.urls.add(u); }); return c; } private void finishUpdatingParameter(CodegenParameter codegenParameter, Parameter parameter) { // default to UNKNOWN_PARAMETER_NAME if paramName is null if (codegenParameter.paramName == null) { LOGGER.warn("Parameter name not defined properly. Default to UNKNOWN_PARAMETER_NAME"); codegenParameter.paramName = "UNKNOWN_PARAMETER_NAME"; } // set the parameter example value // should be overridden by lang codegen setParameterExampleValue(codegenParameter, parameter); // set the parameter examples (if available) setParameterExamples(codegenParameter, parameter); postProcessParameter(codegenParameter); LOGGER.debug("debugging codegenParameter return: {}", codegenParameter); } private void updateParameterForMap(CodegenParameter codegenParameter, Schema parameterSchema, Set imports) { CodegenProperty codegenProperty = fromProperty("inner", ModelUtils.getAdditionalProperties(parameterSchema), false); codegenParameter.items = codegenProperty; codegenParameter.mostInnerItems = codegenProperty.mostInnerItems; codegenParameter.baseType = codegenProperty.dataType; codegenParameter.isContainer = true; codegenParameter.isMap = true; // recursively add import while (codegenProperty != null) { imports.add(codegenProperty.baseType); codegenProperty = codegenProperty.items; } } protected void updateParameterForString(CodegenParameter codegenParameter, Schema parameterSchema) { if (ModelUtils.isEmailSchema(parameterSchema)) { codegenParameter.isEmail = true; } else if (ModelUtils.isUUIDSchema(parameterSchema)) { codegenParameter.isUuid = true; } else if (ModelUtils.isByteArraySchema(parameterSchema)) { codegenParameter.setIsString(false); codegenParameter.isByteArray = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isBinarySchema(parameterSchema)) { codegenParameter.isBinary = true; codegenParameter.isFile = true; // file = binary in OAS3 codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDateSchema(parameterSchema)) { codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDate = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDateTimeSchema(parameterSchema)) { codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDateTime = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDecimalSchema(parameterSchema)) { // type: string, format: number codegenParameter.setIsString(false); codegenParameter.isDecimal = true; codegenParameter.isPrimitiveType = true; } if (Boolean.TRUE.equals(codegenParameter.isString)) { codegenParameter.isPrimitiveType = true; } } /** * Convert OAS Parameter object to Codegen Parameter object * * @param parameter OAS parameter object * @param imports set of imports for library/package/module * @return Codegen Parameter object */ public CodegenParameter fromParameter(Parameter parameter, Set imports) { CodegenParameter codegenParameter = CodegenModelFactory.newInstance(CodegenModelType.PARAMETER); codegenParameter.baseName = parameter.getName(); codegenParameter.description = escapeText(parameter.getDescription()); codegenParameter.unescapedDescription = parameter.getDescription(); if (parameter.getRequired() != null) { codegenParameter.required = parameter.getRequired(); } if (parameter.getDeprecated() != null) { codegenParameter.isDeprecated = parameter.getDeprecated(); } codegenParameter.jsonSchema = Json.pretty(parameter); if (GlobalSettings.getProperty("debugParser") != null) { LOGGER.info("working on Parameter {}", parameter.getName()); LOGGER.info("JSON schema: {}", codegenParameter.jsonSchema); } if (parameter.getExtensions() != null && !parameter.getExtensions().isEmpty()) { codegenParameter.vendorExtensions.putAll(parameter.getExtensions()); } Schema parameterSchema; // the parameter model name is obtained from the schema $ref // e.g. #/components/schemas/list_pageQuery_parameter => toModelName(list_pageQuery_parameter) String parameterModelName = null; if (parameter.getSchema() != null) { parameterSchema = unaliasSchema(parameter.getSchema()); parameterModelName = getParameterDataType(parameter, parameterSchema); CodegenProperty prop; if (this instanceof RustServerCodegen) { // for rust server, we need to do somethings special as it uses // $ref (e.g. #components/schemas/Pet) to determine whether it's a model prop = fromProperty(parameter.getName(), parameterSchema, false); } else if (getUseInlineModelResolver()) { prop = fromProperty(parameter.getName(), getReferencedSchemaWhenNotEnum(parameterSchema), false); } else { prop = fromProperty(parameter.getName(), parameterSchema, false); } codegenParameter.setSchema(prop); } else if (parameter.getContent() != null) { Content content = parameter.getContent(); if (content.size() > 1) { once(LOGGER).warn("Multiple schemas found in content, returning only the first one"); } Map.Entry entry = content.entrySet().iterator().next(); codegenParameter.contentType = entry.getKey(); parameterSchema = entry.getValue().getSchema(); parameterModelName = getParameterDataType(parameter, parameterSchema); } else { parameterSchema = null; } if (parameter instanceof QueryParameter || "query".equalsIgnoreCase(parameter.getIn())) { codegenParameter.isQueryParam = true; codegenParameter.isAllowEmptyValue = parameter.getAllowEmptyValue() != null && parameter.getAllowEmptyValue(); } else if (parameter instanceof PathParameter || "path".equalsIgnoreCase(parameter.getIn())) { codegenParameter.required = true; codegenParameter.isPathParam = true; } else if (parameter instanceof HeaderParameter || "header".equalsIgnoreCase(parameter.getIn())) { codegenParameter.isHeaderParam = true; } else if (parameter instanceof CookieParameter || "cookie".equalsIgnoreCase(parameter.getIn())) { codegenParameter.isCookieParam = true; } else { LOGGER.warn("Unknown parameter type: {}", parameter.getName()); } if (parameterSchema == null) { LOGGER.error("Not handling {} as Body Parameter at the moment", parameter); finishUpdatingParameter(codegenParameter, parameter); return codegenParameter; } // TODO need to review replacing empty map with schemaMapping instead parameterSchema = unaliasSchema(parameterSchema); if (parameterSchema == null) { LOGGER.warn("warning! Schema not found for parameter \" {} \"", parameter.getName()); finishUpdatingParameter(codegenParameter, parameter); return codegenParameter; } if (getUseInlineModelResolver() && !(this instanceof RustServerCodegen)) { // for rust server, we cannot run the following as it uses // $ref (e.g. #components/schemas/Pet) to determine whether it's a model parameterSchema = getReferencedSchemaWhenNotEnum(parameterSchema); } ModelUtils.syncValidationProperties(parameterSchema, codegenParameter); codegenParameter.setTypeProperties(parameterSchema); codegenParameter.setComposedSchemas(getComposedSchemas(parameterSchema)); if (Boolean.TRUE.equals(parameterSchema.getNullable())) { // use nullable defined in the spec codegenParameter.isNullable = true; } if (parameter.getStyle() != null) { codegenParameter.style = parameter.getStyle().toString(); codegenParameter.isDeepObject = Parameter.StyleEnum.DEEPOBJECT == parameter.getStyle(); codegenParameter.isMatrix = Parameter.StyleEnum.MATRIX == parameter.getStyle(); } // the default value is false // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#user-content-parameterexplode codegenParameter.isExplode = parameter.getExplode() != null && parameter.getExplode(); // TODO revise collectionFormat, default collection format in OAS 3 appears to multi at least for query parameters // https://swagger.io/docs/specification/serialization/ String collectionFormat = null; if (ModelUtils.isFileSchema(parameterSchema) && !ModelUtils.isStringSchema(parameterSchema)) { // swagger v2 only, type file codegenParameter.isFile = true; } else if (ModelUtils.isStringSchema(parameterSchema)) { updateParameterForString(codegenParameter, parameterSchema); } else if (ModelUtils.isBooleanSchema(parameterSchema)) { codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isNumberSchema(parameterSchema)) { codegenParameter.isPrimitiveType = true; if (ModelUtils.isFloatSchema(parameterSchema)) { // float codegenParameter.isFloat = true; } else if (ModelUtils.isDoubleSchema(parameterSchema)) { // double codegenParameter.isDouble = true; } } else if (ModelUtils.isIntegerSchema(parameterSchema)) { // integer type codegenParameter.isPrimitiveType = true; if (ModelUtils.isLongSchema(parameterSchema)) { // int64/long format codegenParameter.isLong = true; } else { codegenParameter.isInteger = true; if (ModelUtils.isShortSchema(parameterSchema)) { // int32/short format codegenParameter.isShort = true; } else { // unbounded integer } } } else if (ModelUtils.isTypeObjectSchema(parameterSchema)) { if (ModelUtils.isMapSchema(parameterSchema)) { // for map parameter updateParameterForMap(codegenParameter, parameterSchema, imports); } if (ModelUtils.isFreeFormObject(parameterSchema)) { codegenParameter.isFreeFormObject = true; } addVarsRequiredVarsAdditionalProps(parameterSchema, codegenParameter); } else if (ModelUtils.isNullType(parameterSchema)) { } else if (ModelUtils.isAnyType(parameterSchema)) { // any schema with no type set, composed schemas often do this if (ModelUtils.isMapSchema(parameterSchema)) { // for map parameter updateParameterForMap(codegenParameter, parameterSchema, imports); } addVarsRequiredVarsAdditionalProps(parameterSchema, codegenParameter); } else if (ModelUtils.isArraySchema(parameterSchema)) { final ArraySchema arraySchema = (ArraySchema) parameterSchema; Schema inner = getSchemaItems(arraySchema); collectionFormat = getCollectionFormat(parameter); // default to csv: collectionFormat = StringUtils.isEmpty(collectionFormat) ? "csv" : collectionFormat; CodegenProperty itemsProperty = fromProperty("inner", inner, false); codegenParameter.items = itemsProperty; codegenParameter.mostInnerItems = itemsProperty.mostInnerItems; codegenParameter.baseType = itemsProperty.dataType; codegenParameter.isContainer = true; // recursively add import while (itemsProperty != null) { imports.add(itemsProperty.baseType); itemsProperty = itemsProperty.items; } } else { // referenced schemas } CodegenProperty codegenProperty = fromProperty(parameter.getName(), parameterSchema, false); if (Boolean.TRUE.equals(codegenProperty.isModel)) { codegenParameter.isModel = true; } if (parameterModelName != null) { codegenParameter.dataType = parameterModelName; if (ModelUtils.isObjectSchema(parameterSchema) || ModelUtils.isComposedSchema(parameterSchema)) { codegenProperty.complexType = codegenParameter.dataType; } } else { codegenParameter.dataType = codegenProperty.dataType; } if (ModelUtils.isArraySchema(parameterSchema)) { imports.add(codegenProperty.baseType); } codegenParameter.dataFormat = codegenProperty.dataFormat; if (parameter.getRequired() != null) { codegenParameter.required = parameter.getRequired().booleanValue(); } // set containerType codegenParameter.containerType = codegenProperty.containerType; codegenParameter.containerTypeMapped = codegenProperty.containerTypeMapped; // enum updateCodegenPropertyEnum(codegenProperty); codegenParameter.isEnum = codegenProperty.isEnum; codegenParameter.isEnumRef = codegenProperty.isEnumRef; codegenParameter._enum = codegenProperty._enum; codegenParameter.allowableValues = codegenProperty.allowableValues; if (codegenProperty.isEnum) { codegenParameter.datatypeWithEnum = codegenProperty.datatypeWithEnum; codegenParameter.enumName = codegenProperty.enumName; if (codegenProperty.defaultValue != null) { codegenParameter.enumDefaultValue = codegenProperty.defaultValue.replace(codegenProperty.enumName + ".", ""); } } if (codegenProperty.items != null && codegenProperty.items.isEnum) { codegenParameter.datatypeWithEnum = codegenProperty.datatypeWithEnum; codegenParameter.enumName = codegenProperty.enumName; codegenParameter.items = codegenProperty.items; codegenParameter.mostInnerItems = codegenProperty.mostInnerItems; } codegenParameter.collectionFormat = collectionFormat; if ("multi".equals(collectionFormat)) { codegenParameter.isCollectionFormatMulti = true; } codegenParameter.paramName = toParamName(parameter.getName()); // import if (codegenProperty.complexType != null) { imports.add(codegenProperty.complexType); } codegenParameter.pattern = toRegularExpression(parameterSchema.getPattern()); if (codegenParameter.isQueryParam && codegenParameter.isDeepObject && loadDeepObjectIntoItems) { Schema schema = parameterSchema; if (schema.get$ref() != null) { schema = ModelUtils.getReferencedSchema(openAPI, schema); } codegenParameter.items = fromProperty(codegenParameter.paramName, schema, false); // https://swagger.io/docs/specification/serialization/ if (schema != null) { Map> properties = schema.getProperties(); List requiredVarNames = new ArrayList<>(); if (schema.getRequired() != null) { requiredVarNames.addAll(schema.getRequired()); } if (properties != null) { codegenParameter.items.vars = properties.entrySet().stream() .map(entry -> { CodegenProperty property = fromProperty(entry.getKey(), entry.getValue(), requiredVarNames.contains(entry.getKey())); return property; }).collect(Collectors.toList()); } else { //LOGGER.error("properties is null: {}", schema); } } else { LOGGER.warn( "No object schema found for deepObject parameter{} deepObject won't have specific properties", codegenParameter); } } // set default value codegenParameter.defaultValue = toDefaultParameterValue(codegenProperty, parameterSchema); finishUpdatingParameter(codegenParameter, parameter); return codegenParameter; } private Schema getReferencedSchemaWhenNotEnum(Schema parameterSchema) { Schema referencedSchema = ModelUtils.getReferencedSchema(openAPI, parameterSchema); if (referencedSchema.getEnum() != null && !referencedSchema.getEnum().isEmpty()) { referencedSchema = parameterSchema; } return referencedSchema; } /** * Returns the data type of parameter. * Returns null by default to use the CodegenProperty.datatype value * * @param parameter Parameter * @param schema Schema * @return data type */ protected String getParameterDataType(Parameter parameter, Schema schema) { Schema unaliasSchema = unaliasSchema(schema); if (unaliasSchema.get$ref() != null) { return toModelName(ModelUtils.getSimpleRef(unaliasSchema.get$ref())); } return null; } // TODO revise below as it should be replaced by ModelUtils.isByteArraySchema(parameterSchema) public boolean isDataTypeBinary(String dataType) { if (dataType != null) { return dataType.toLowerCase(Locale.ROOT).startsWith("byte"); } else { return false; } } // TODO revise below as it should be replaced by ModelUtils.isFileSchema(parameterSchema) public boolean isDataTypeFile(String dataType) { if (dataType != null) { return dataType.toLowerCase(Locale.ROOT).equals("file"); } else { return false; } } /** * Convert map of OAS SecurityScheme objects to a list of Codegen Security objects * * @param securitySchemeMap a map of OAS SecuritySchemeDefinition object * @return a list of Codegen Security objects */ @Override @SuppressWarnings("static-method") public List fromSecurity(Map securitySchemeMap) { if (securitySchemeMap == null) { return Collections.emptyList(); } List codegenSecurities = new ArrayList<>(securitySchemeMap.size()); for (String key : securitySchemeMap.keySet()) { final SecurityScheme securityScheme = securitySchemeMap.get(key); if (SecurityScheme.Type.APIKEY.equals(securityScheme.getType())) { final CodegenSecurity cs = defaultCodegenSecurity(key, securityScheme); cs.isBasic = cs.isOAuth = cs.isOpenId = false; cs.isApiKey = true; cs.keyParamName = securityScheme.getName(); cs.isKeyInHeader = securityScheme.getIn() == SecurityScheme.In.HEADER; cs.isKeyInQuery = securityScheme.getIn() == SecurityScheme.In.QUERY; cs.isKeyInCookie = securityScheme.getIn() == SecurityScheme.In.COOKIE; //it assumes a validation step prior to generation. (cookie-auth supported from OpenAPI 3.0.0) codegenSecurities.add(cs); } else if (SecurityScheme.Type.HTTP.equals(securityScheme.getType())) { final CodegenSecurity cs = defaultCodegenSecurity(key, securityScheme); cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isOAuth = cs.isOpenId = false; cs.isBasic = true; if ("basic".equalsIgnoreCase(securityScheme.getScheme())) { cs.isBasicBasic = true; } else if ("bearer".equalsIgnoreCase(securityScheme.getScheme())) { cs.isBasicBearer = true; cs.bearerFormat = securityScheme.getBearerFormat(); } else if ("signature".equalsIgnoreCase(securityScheme.getScheme())) { // HTTP signature as defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ // The registry of security schemes is maintained by IANA. // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml // As of January 2020, the "signature" scheme has not been registered with IANA yet. // This scheme may have to be changed when it is officially registered with IANA. cs.isHttpSignature = true; once(LOGGER).warn("Security scheme 'HTTP signature' is a draft IETF RFC and subject to change."); } else { once(LOGGER).warn("Unknown scheme `{}` found in the HTTP security definition.", securityScheme.getScheme()); } codegenSecurities.add(cs); } else if (SecurityScheme.Type.OAUTH2.equals(securityScheme.getType())) { final OAuthFlows flows = securityScheme.getFlows(); boolean isFlowEmpty = true; if (securityScheme.getFlows() == null) { throw new RuntimeException("missing oauth flow in " + key); } if (flows.getPassword() != null) { final CodegenSecurity cs = defaultOauthCodegenSecurity(key, securityScheme); setOauth2Info(cs, flows.getPassword()); cs.isPassword = true; cs.flow = "password"; codegenSecurities.add(cs); isFlowEmpty = false; } if (flows.getImplicit() != null) { final CodegenSecurity cs = defaultOauthCodegenSecurity(key, securityScheme); setOauth2Info(cs, flows.getImplicit()); cs.isImplicit = true; cs.flow = "implicit"; codegenSecurities.add(cs); isFlowEmpty = false; } if (flows.getClientCredentials() != null) { final CodegenSecurity cs = defaultOauthCodegenSecurity(key, securityScheme); setOauth2Info(cs, flows.getClientCredentials()); cs.isApplication = true; cs.flow = "application"; codegenSecurities.add(cs); isFlowEmpty = false; } if (flows.getAuthorizationCode() != null) { final CodegenSecurity cs = defaultOauthCodegenSecurity(key, securityScheme); setOauth2Info(cs, flows.getAuthorizationCode()); cs.isCode = true; cs.flow = "accessCode"; codegenSecurities.add(cs); isFlowEmpty = false; } if (isFlowEmpty) { once(LOGGER).error("Invalid flow definition defined in the security scheme: {}", flows); } } else if (SecurityScheme.Type.OPENIDCONNECT.equals(securityScheme.getType())) { final CodegenSecurity cs = defaultCodegenSecurity(key, securityScheme); cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = false; cs.isOpenId = true; cs.openIdConnectUrl = securityScheme.getOpenIdConnectUrl(); if (securityScheme.getFlows() != null) { setOpenIdConnectInfo(cs, securityScheme.getFlows().getAuthorizationCode()); } codegenSecurities.add(cs); } else { once(LOGGER).error("Unknown type `{}` found in the security definition `{}`.", securityScheme.getType(), securityScheme.getName()); } } return codegenSecurities; } private CodegenSecurity defaultCodegenSecurity(String key, SecurityScheme securityScheme) { final CodegenSecurity cs = CodegenModelFactory.newInstance(CodegenModelType.SECURITY); cs.name = key; cs.description = securityScheme.getDescription(); cs.type = securityScheme.getType().toString(); cs.isCode = cs.isPassword = cs.isApplication = cs.isImplicit = cs.isOpenId = false; cs.isHttpSignature = false; cs.isBasicBasic = cs.isBasicBearer = false; cs.scheme = securityScheme.getScheme(); if (securityScheme.getExtensions() != null) { cs.vendorExtensions.putAll(securityScheme.getExtensions()); } return cs; } private CodegenSecurity defaultOauthCodegenSecurity(String key, SecurityScheme securityScheme) { final CodegenSecurity cs = defaultCodegenSecurity(key, securityScheme); cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = cs.isOpenId = false; cs.isOAuth = true; return cs; } protected void setReservedWordsLowerCase(List words) { reservedWords = new HashSet<>(); for (String word : words) { reservedWords.add(word.toLowerCase(Locale.ROOT)); } } protected boolean isReservedWord(String word) { return word != null && reservedWords.contains(word.toLowerCase(Locale.ROOT)); } /** * Get operationId from the operation object, and if it's blank, generate a new one from the given parameters. * * @param operation the operation object * @param path the path of the operation * @param httpMethod the HTTP method of the operation * @return the (generated) operationId */ protected String getOrGenerateOperationId(Operation operation, String path, String httpMethod) { String operationId = operation.getOperationId(); if (StringUtils.isBlank(operationId)) { String tmpPath = path; tmpPath = tmpPath.replaceAll("\\{", ""); tmpPath = tmpPath.replaceAll("\\}", ""); String[] parts = (tmpPath + "/" + httpMethod).split("/"); StringBuilder builder = new StringBuilder(); if ("/".equals(tmpPath)) { // must be root tmpPath builder.append("root"); } for (String part : parts) { if (part.length() > 0) { if (builder.toString().length() == 0) { part = Character.toLowerCase(part.charAt(0)) + part.substring(1); } else { part = camelize(part); } builder.append(part); } } operationId = sanitizeName(builder.toString()); LOGGER.warn("Empty operationId found for path: {} {}. Renamed to auto-generated operationId: {}", httpMethod, path, operationId); } return operationId; } /** * Check the type to see if it needs import the library/module/package * * @param type name of the type * @return true if the library/module/package of the corresponding type needs to be imported */ protected boolean needToImport(String type) { return StringUtils.isNotBlank(type) && !defaultIncludes.contains(type) && !languageSpecificPrimitives.contains(type); } @SuppressWarnings("static-method") protected List> toExamples(Map examples) { if (examples == null) { return null; } final List> output = new ArrayList<>(examples.size()); for (Map.Entry entry : examples.entrySet()) { final Map kv = new HashMap<>(); kv.put("contentType", entry.getKey()); kv.put("example", entry.getValue()); output.add(kv); } return output; } /** * Add headers to codegen property * * @param response API response * @param properties list of codegen property */ protected void addHeaders(ApiResponse response, List properties) { if (response.getHeaders() != null) { for (Map.Entry headerEntry : response.getHeaders().entrySet()) { String description = headerEntry.getValue().getDescription(); // follow the $ref Header header = ModelUtils.getReferencedHeader(this.openAPI, headerEntry.getValue()); Schema schema; if (header.getSchema() == null) { LOGGER.warn("No schema defined for Header '{}', using a String schema", headerEntry.getKey()); schema = new StringSchema(); } else { schema = header.getSchema(); } CodegenProperty cp = fromProperty(headerEntry.getKey(), schema, false); cp.setDescription(escapeText(description)); cp.setUnescapedDescription(description); if (header.getRequired() != null) { cp.setRequired(header.getRequired()); } else { cp.setRequired(false); } properties.add(cp); } } } /** * Add operation to group * * @param tag name of the tag * @param resourcePath path of the resource * @param operation OAS Operation object * @param co Codegen Operation object * @param operations map of Codegen operations */ @Override @SuppressWarnings("static-method") public void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map> operations) { List opList = operations.get(tag); if (opList == null) { opList = new ArrayList<>(); operations.put(tag, opList); } // check for operationId uniqueness String uniqueName = co.operationId; int counter = 0; for (CodegenOperation op : opList) { if (uniqueName.equals(op.operationId)) { uniqueName = co.operationId + "_" + counter; counter++; } } if (!co.operationId.equals(uniqueName)) { LOGGER.warn("generated unique operationId `{}`", uniqueName); } co.operationId = uniqueName; co.operationIdLowerCase = uniqueName.toLowerCase(Locale.ROOT); co.operationIdCamelCase = camelize(uniqueName); co.operationIdSnakeCase = underscore(uniqueName); opList.add(co); co.baseName = tag; } protected void addParentFromContainer(CodegenModel model, Schema schema) { model.parent = toInstantiationType(schema); } /** * Sets the value of the 'model.parent' property in CodegenModel, based on the value * of the 'additionalProperties' keyword. Some language generator use class inheritance * to implement additional properties. For example, in Java the generated model class * has 'extends HashMap' to represent the additional properties. *

* TODO: it's not a good idea to use single class inheritance to implement * additionalProperties. That may work for non-composed schemas, but that does not * work for composed 'allOf' schemas. For example, in Java, if additionalProperties * is set to true (which it should be by default, per OAS spec), then the generated * code has extends HashMap. That wouldn't work for composed 'allOf' schemas. * * @param model the codegen representation of the OAS schema. * @param name the name of the model. * @param schema the input OAS schema. */ protected void addParentContainer(CodegenModel model, String name, Schema schema) { final CodegenProperty property = fromProperty(name, schema, false); addImport(model, property.complexType); addParentFromContainer(model, schema); final String instantiationType = instantiationTypes.get(property.containerType); if (instantiationType != null) { addImport(model, instantiationType); } if (property.containerTypeMapped != null) { addImport(model, property.containerTypeMapped); } } /** * Generate the next name for the given name, i.e. append "2" to the base name if not ending with a number, * otherwise increase the number by 1. For example: * status => status2 * status2 => status3 * myName100 => myName101 * * @param name The base name * @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); if (matcher.find()) { String numStr = matcher.group(); int num = Integer.parseInt(numStr) + 1; return name.substring(0, name.length() - numStr.length()) + num; } else { return name + "2"; } } protected void addImports(CodegenModel m, IJsonSchemaValidationProperties type) { addImports(m.imports, type); } protected void addImports(Set importsToBeAddedTo, IJsonSchemaValidationProperties type) { addImports(importsToBeAddedTo, type.getImports(importContainerType, importBaseType, generatorMetadata.getFeatureSet())); } protected void addImports(Set importsToBeAddedTo, Set importsToAdd) { importsToAdd.stream().forEach(i -> addImport(importsToBeAddedTo, i)); } protected void addImport(CodegenModel m, String type) { addImport(m.imports, type); } protected void addImport(Set importsToBeAddedTo, String type) { if (shouldAddImport(type)) { importsToBeAddedTo.add(type); } } /** * Add the model name of the child schema in a composed schema to the set of imports * * @param composed composed schema * @param childSchema composed schema * @param model codegen model * @param modelName model name */ protected void addImport(Schema composed, Schema childSchema, CodegenModel model, String modelName) { if (composed == null || childSchema == null) { return; } // import only if it's not allOf composition schema (without discriminator) if (!(composed.getAllOf() != null && childSchema.getDiscriminator() == null)) { addImport(model, modelName); } else { LOGGER.debug("Skipped import for allOf composition schema {}", modelName); } } protected boolean shouldAddImport(String type) { return type != null && needToImport(type); } /** * Loop through properties and unalias the reference if $ref (reference) is defined * * @param properties model properties (schemas) * @return model properties with direct reference to schemas */ protected Map unaliasPropertySchema(Map properties) { if (properties != null) { for (String key : properties.keySet()) { properties.put(key, unaliasSchema(properties.get(key))); } } return properties; } protected void addVars(CodegenModel m, Map properties, List required, Map allProperties, List allRequired) { m.hasRequired = false; if (properties != null && !properties.isEmpty()) { m.hasVars = true; Set mandatory = required == null ? Collections.emptySet() : new TreeSet<>(required); // update "vars" without parent's properties (all, required) addVars(m, m.vars, properties, mandatory); m.allMandatory = m.mandatory = mandatory; } else { m.emptyVars = true; m.hasVars = false; m.hasEnums = false; } if (allProperties != null) { Set allMandatory = allRequired == null ? Collections.emptySet() : new TreeSet<>(allRequired); // update "allVars" with parent's properties (all, required) addVars(m, m.allVars, allProperties, allMandatory); m.allMandatory = allMandatory; } else { // without parent, allVars and vars are the same m.allVars = m.vars; m.allMandatory = m.mandatory; } // loop through list to update property name with toVarName Set renamedMandatory = new ConcurrentSkipListSet<>(); Iterator mandatoryIterator = m.mandatory.iterator(); while (mandatoryIterator.hasNext()) { renamedMandatory.add(toVarName(mandatoryIterator.next())); } m.mandatory = renamedMandatory; Set renamedAllMandatory = new ConcurrentSkipListSet<>(); Iterator allMandatoryIterator = m.allMandatory.iterator(); while (allMandatoryIterator.hasNext()) { renamedAllMandatory.add(toVarName(allMandatoryIterator.next())); } m.allMandatory = renamedAllMandatory; } /** * Add variables (properties) to codegen model (list of properties, various flags, etc) * * @param m Must be an instance of IJsonSchemaValidationProperties, may be model or property... * @param vars list of codegen properties (e.g. vars, allVars) to be updated with the new properties * @param properties a map of properties (schema) * @param mandatory a set of required properties' name */ protected void addVars(IJsonSchemaValidationProperties m, List vars, Map properties, Set mandatory) { if (properties == null) { return; } HashMap varsMap = new HashMap<>(); CodegenModel cm = null; if (m instanceof CodegenModel) { cm = (CodegenModel) m; if (cm.allVars == vars) { // processing allVars for (CodegenProperty var : cm.vars) { // create a map of codegen properties for lookup later varsMap.put(var.baseName, var); var.isOverridden = false; } } } for (Map.Entry entry : properties.entrySet()) { final String key = entry.getKey(); final Schema prop = entry.getValue(); if (prop == null) { LOGGER.warn("Please report the issue. There shouldn't be null property for {}", key); } else { final CodegenProperty cp; if (cm != null && cm.allVars == vars && varsMap.keySet().contains(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 LOGGER.debug("The property `{}` already defined in the child model. Using the one from child.", key); cp = varsMap.get(key); } else { // properties in the parent model only cp = fromProperty(key, prop, mandatory.contains(key)); } if (cm != null && cm.allVars == vars && cp.isOverridden == null) { // processing allVars and it's a parent property cp.isOverridden = true; } vars.add(cp); m.setHasVars(true); if (cp.required) { m.setHasRequired(true); m.getRequiredVars().add(cp); } if (cm == null) { continue; } cm.hasOptional = cm.hasOptional || !cp.required; if (cp.getIsEnumOrRef()) { // isEnum or isEnumRef set to true // FIXME: if supporting inheritance, when called a second time for allProperties it is possible for // m.hasEnums to be set incorrectly if allProperties has enumerations but properties does not. cm.hasEnums = true; } // set model's hasOnlyReadOnly to false if the property is read-only if (!Boolean.TRUE.equals(cp.isReadOnly)) { cm.hasOnlyReadOnly = false; } addImportsForPropertyType(cm, cp); // if required, add to the list "requiredVars" if (Boolean.FALSE.equals(cp.required)) { cm.optionalVars.add(cp); } // if readonly, add to readOnlyVars (list of properties) if (Boolean.TRUE.equals(cp.isReadOnly)) { cm.readOnlyVars.add(cp); } else { // else add to readWriteVars (list of properties) // duplicated properties will be removed by removeAllDuplicatedProperty later cm.readWriteVars.add(cp); } if (Boolean.FALSE.equals(cp.isNullable)) { cm.nonNullableVars.add(cp); } } } return; } /** * For a given property, adds all needed imports to the model * This includes a flat property type (e.g. property type: ReferencedModel) * as well as container type (property type: array of ReferencedModel's) * * @param model The codegen representation of the OAS schema. * @param property The codegen representation of the OAS schema's property. */ protected void addImportsForPropertyType(CodegenModel model, CodegenProperty property) { if (property.isArray) { if (Boolean.TRUE.equals(property.getUniqueItemsBoolean())) { // set addImport(model.imports, typeMapping.get("set")); } else { // array addImport(model.imports, typeMapping.get("array")); } } if (property.isMap) { // map addImport(model.imports, typeMapping.get("map")); } addImports(model, property); } /** * Determine all of the types in the model definitions (schemas) that are aliases of * simple types. * * @param schemas The complete set of model definitions (schemas). * @return A mapping from model name to type alias */ Map getAllAliases(Map schemas) { if (schemas == null || schemas.isEmpty()) { return new HashMap<>(); } Map aliases = new HashMap<>(); for (Map.Entry entry : schemas.entrySet()) { Schema schema = entry.getValue(); if (isAliasOfSimpleTypes(schema)) { if (schema.getAllOf() != null && schema.getAllOf().size() == 1) { // allOf with a single item Schema unaliasSchema = unaliasSchema(schema); unaliasSchema = ModelUtils.getReferencedSchema(this.openAPI, unaliasSchema); aliases.put(entry.getKey() /* schema name, e.g. Pet */, getPrimitiveType(unaliasSchema)); } else { aliases.put(entry.getKey() /* schema name, e.g. Pet */, getPrimitiveType(schema)); } } } return aliases; } private Boolean isAliasOfSimpleTypes(Schema schema) { if (schema == null) { return false; } // allOf with a single item if (schema.getAllOf() != null && schema.getAllOf().size() == 1 && schema.getAllOf().get(0) instanceof Schema) { schema = unaliasSchema((Schema) schema.getAllOf().get(0)); schema = ModelUtils.getReferencedSchema(this.openAPI, schema); } return (!ModelUtils.isObjectSchema(schema) && !ModelUtils.isArraySchema(schema) && !ModelUtils.isMapSchema(schema) && !ModelUtils.isComposedSchema(schema) && schema.getEnum() == null); } /** * Remove characters not suitable for variable or method name from the input and camelize it * * @param name string to be camelize * @return camelized string */ @SuppressWarnings("static-method") public String removeNonNameElementToCamelCase(String name) { return removeNonNameElementToCamelCase(name, "[-_:;#" + removeOperationIdPrefixDelimiter + "]"); } /** * Remove characters that is not good to be included in method name from the input and camelize it * * @param name string to be camelize * @param nonNameElementPattern a regex pattern of the characters that is not good to be included in name * @return camelized string */ protected String removeNonNameElementToCamelCase(final String name, final String nonNameElementPattern) { String result = Arrays.stream(name.split(nonNameElementPattern)) .map(StringUtils::capitalize) .collect(Collectors.joining("")); if (result.length() > 0) { result = result.substring(0, 1).toLowerCase(Locale.ROOT) + result.substring(1); } return result; } @Override public String apiFilename(String templateName, String tag) { String suffix = apiTemplateFiles().get(templateName); return apiFileFolder() + File.separator + toApiFilename(tag) + suffix; } @Override public String apiFilename(String templateName, String tag, String outputDir) { String suffix = apiTemplateFiles().get(templateName); return outputDir + File.separator + toApiFilename(tag) + suffix; } @Override public String modelFilename(String templateName, String modelName) { String suffix = modelTemplateFiles().get(templateName); return modelFileFolder() + File.separator + toModelFilename(modelName) + suffix; } @Override public String modelFilename(String templateName, String modelName, String outputDir) { String suffix = modelTemplateFiles().get(templateName); return outputDir + File.separator + toModelFilename(modelName) + suffix; } /** * Return the full path and API documentation file * * @param templateName template name * @param tag tag * @return the API documentation file name with full path */ @Override public String apiDocFilename(String templateName, String tag) { String docExtension = getDocExtension(); String suffix = docExtension != null ? docExtension : apiDocTemplateFiles().get(templateName); return apiDocFileFolder() + File.separator + toApiDocFilename(tag) + suffix; } /** * Return the full path and API test file * * @param templateName template name * @param tag tag * @return the API test file name with full path */ @Override public String apiTestFilename(String templateName, String tag) { String suffix = apiTestTemplateFiles().get(templateName); return apiTestFileFolder() + File.separator + toApiTestFilename(tag) + suffix; } @Override public boolean shouldOverwrite(String filename) { return !(skipOverwrite && new File(filename).exists()); } @Override public boolean isSkipOverwrite() { return skipOverwrite; } @Override public void setSkipOverwrite(boolean skipOverwrite) { this.skipOverwrite = skipOverwrite; } @Override public boolean isRemoveOperationIdPrefix() { return removeOperationIdPrefix; } @Override public boolean isSkipOperationExample() { return skipOperationExample; } @Override public void setRemoveOperationIdPrefix(boolean removeOperationIdPrefix) { this.removeOperationIdPrefix = removeOperationIdPrefix; } public String getRemoveOperationIdPrefixDelimiter() { return removeOperationIdPrefixDelimiter; } public void setRemoveOperationIdPrefixDelimiter(String removeOperationIdPrefixDelimiter) { this.removeOperationIdPrefixDelimiter = removeOperationIdPrefixDelimiter; } public int getRemoveOperationIdPrefixCount() { return removeOperationIdPrefixCount; } public void setRemoveOperationIdPrefixCount(int removeOperationIdPrefixCount) { this.removeOperationIdPrefixCount = removeOperationIdPrefixCount; } @Override public void setSkipOperationExample(boolean skipOperationExample) { this.skipOperationExample = skipOperationExample; } @Override public boolean isHideGenerationTimestamp() { return hideGenerationTimestamp; } @Override public void setHideGenerationTimestamp(boolean hideGenerationTimestamp) { this.hideGenerationTimestamp = hideGenerationTimestamp; } /** * All library templates supported. * (key: library name, value: library description) * * @return the supported libraries */ @Override public Map supportedLibraries() { return supportedLibraries; } /** * Set library template (sub-template). * * @param library Library template */ @Override public void setLibrary(String library) { if (library != null && !supportedLibraries.containsKey(library)) { StringBuilder sb = new StringBuilder("Unknown library: " + library + "\nAvailable libraries:"); if (supportedLibraries.size() == 0) { sb.append("\n ").append("NONE"); } else { for (String lib : supportedLibraries.keySet()) { sb.append("\n ").append(lib); } } throw new RuntimeException(sb.toString()); } this.library = library; } /** * Library template (sub-template). * * @return Library template */ @Override public String getLibrary() { return library; } /** * check if current active library equals to passed * * @param library - library to be compared with * @return {@code true} if passed library is active, {@code false} otherwise */ public final boolean isLibrary(String library) { return library.equals(this.library); } /** * Set Git host. * * @param gitHost Git host */ @Override public void setGitHost(String gitHost) { this.gitHost = gitHost; } /** * Git host. * * @return Git host */ @Override public String getGitHost() { return gitHost; } /** * Set Git user ID. * * @param gitUserId Git user ID */ @Override public void setGitUserId(String gitUserId) { this.gitUserId = gitUserId; } /** * Git user ID * * @return Git user ID */ @Override public String getGitUserId() { return gitUserId; } /** * Set Git repo ID. * * @param gitRepoId Git repo ID */ @Override public void setGitRepoId(String gitRepoId) { this.gitRepoId = gitRepoId; } /** * Git repo ID * * @return Git repo ID */ @Override public String getGitRepoId() { return gitRepoId; } /** * Set release note. * * @param releaseNote Release note */ @Override public void setReleaseNote(String releaseNote) { this.releaseNote = releaseNote; } /** * Release note * * @return Release note */ @Override public String getReleaseNote() { return releaseNote; } /** * Documentation files extension * * @return Documentation files extension */ @Override public String getDocExtension() { return docExtension; } /** * Set Documentation files extension * * @param userDocExtension documentation files extension */ @Override public void setDocExtension(String userDocExtension) { this.docExtension = userDocExtension; } /** * Set HTTP user agent. * * @param httpUserAgent HTTP user agent */ @Override public void setHttpUserAgent(String httpUserAgent) { this.httpUserAgent = httpUserAgent; } /** * HTTP user agent * * @return HTTP user agent */ @Override public String getHttpUserAgent() { return httpUserAgent; } @SuppressWarnings("static-method") protected CliOption buildLibraryCliOption(Map supportedLibraries) { StringBuilder sb = new StringBuilder("library template (sub-template) to use:"); for (String lib : supportedLibraries.keySet()) { sb.append("\n").append(lib).append(" - ").append(supportedLibraries.get(lib)); } return new CliOption(CodegenConstants.LIBRARY, sb.toString()); } /** * Sanitize name (parameter, property, method, etc) * * @param name string to be sanitize * @return sanitized string */ @Override @SuppressWarnings("static-method") public String sanitizeName(String name) { return sanitizeName(name, "\\W"); } @Override public void setTemplatingEngine(TemplatingEngineAdapter templatingEngine) { this.templatingEngine = templatingEngine; } @Override public TemplatingEngineAdapter getTemplatingEngine() { return this.templatingEngine; } /** * Sanitize name (parameter, property, method, etc) * * @param name string to be sanitize * @param removeCharRegEx a regex containing all char that will be removed * @return sanitized string */ public String sanitizeName(String name, String removeCharRegEx) { return sanitizeName(name, removeCharRegEx, new ArrayList<>()); } /** * Sanitize name (parameter, property, method, etc) * * @param name string to be sanitize * @param removeCharRegEx a regex containing all char that will be removed * @param exceptionList a list of matches which should not be sanitized (i.e exception) * @return sanitized string */ @SuppressWarnings("static-method") public String sanitizeName(final String name, String removeCharRegEx, ArrayList exceptionList) { // NOTE: performance wise, we should have written with 2 replaceAll to replace desired // character with _ or empty character. Below aims to spell out different cases we've // encountered so far and hopefully make it easier for others to add more special // cases in the future. // better error handling when map/array type is invalid if (name == null) { LOGGER.error("String to be sanitized is null. Default to ERROR_UNKNOWN"); return "ERROR_UNKNOWN"; } // if the name is just '$', map it to 'value' for the time being. if ("$".equals(name)) { return "value"; } SanitizeNameOptions opts = new SanitizeNameOptions(name, removeCharRegEx, exceptionList); return sanitizedNameCache.get(opts, sanitizeNameOptions -> { String modifiable = sanitizeNameOptions.getName(); List exceptions = sanitizeNameOptions.getExceptions(); // input[] => input modifiable = this.sanitizeValue(modifiable, "\\[\\]", "", exceptions); // input[a][b] => input_a_b modifiable = this.sanitizeValue(modifiable, "\\[", "_", exceptions); modifiable = this.sanitizeValue(modifiable, "\\]", "", exceptions); // input(a)(b) => input_a_b modifiable = this.sanitizeValue(modifiable, "\\(", "_", exceptions); modifiable = this.sanitizeValue(modifiable, "\\)", "", exceptions); // input.name => input_name modifiable = this.sanitizeValue(modifiable, "\\.", "_", exceptions); // input-name => input_name modifiable = this.sanitizeValue(modifiable, "-", "_", exceptions); // a|b => a_b modifiable = this.sanitizeValue(modifiable, "\\|", "_", exceptions); // input name and age => input_name_and_age modifiable = this.sanitizeValue(modifiable, " ", "_", exceptions); // /api/films/get => _api_films_get // \api\films\get => _api_films_get modifiable = modifiable.replaceAll("/", "_"); modifiable = modifiable.replaceAll("\\\\", "_"); // 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(""); } else { modifiable = modifiable.replaceAll(sanitizeNameOptions.getRemoveCharRegEx(), ""); } return modifiable; }); } private String sanitizeValue(String value, String replaceMatch, String replaceValue, List exceptionList) { if (exceptionList.size() == 0 || !exceptionList.contains(replaceMatch)) { return value.replaceAll(replaceMatch, replaceValue); } return value; } /** * Sanitize tag * * @param tag Tag * @return Sanitized tag */ @Override public String sanitizeTag(String tag) { tag = camelize(sanitizeName(tag)); // tag starts with numbers if (tag.matches("^\\d.*")) { tag = "Class" + tag; } return tag; } /** * Set CodegenParameter boolean flag using CodegenProperty. * NOTE: This is deprecated and can be removed in 6.0.0 * This logic has been folded into the original call sites and long term will be moved into * IJsonSchemaValidationProperties.setTypeProperties and overrides like updateModelForObject * * @param parameter Codegen Parameter * @param property Codegen property */ public void setParameterBooleanFlagWithCodegenProperty(CodegenParameter parameter, CodegenProperty property) { if (parameter == null) { LOGGER.error("Codegen Parameter cannot be null."); return; } if (property == null) { LOGGER.error("Codegen Property cannot be null."); return; } if (Boolean.TRUE.equals(property.isEmail) && Boolean.TRUE.equals(property.isString)) { parameter.isEmail = true; } else if (Boolean.TRUE.equals(property.isPassword) && Boolean.TRUE.equals(property.isString)) { parameter.isPassword = true; } else if (Boolean.TRUE.equals(property.isUuid) && Boolean.TRUE.equals(property.isString)) { parameter.isUuid = true; } else if (Boolean.TRUE.equals(property.isByteArray)) { parameter.isByteArray = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isBinary)) { parameter.isBinary = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isString)) { parameter.isString = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isBoolean)) { parameter.isBoolean = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isLong)) { parameter.isLong = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isInteger)) { parameter.isInteger = true; parameter.isPrimitiveType = true; if (Boolean.TRUE.equals(property.isShort)) { parameter.isShort = true; } else if (Boolean.TRUE.equals(property.isUnboundedInteger)) { parameter.isUnboundedInteger = true; } } else if (Boolean.TRUE.equals(property.isDouble)) { parameter.isDouble = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isFloat)) { parameter.isFloat = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isDecimal)) { parameter.isDecimal = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isNumber)) { parameter.isNumber = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isDate)) { parameter.isDate = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isDateTime)) { parameter.isDateTime = true; parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isFreeFormObject)) { parameter.isFreeFormObject = true; } else if (Boolean.TRUE.equals(property.isAnyType)) { parameter.isAnyType = true; } else { LOGGER.debug("Property type is not primitive: {}", property.dataType); } if (Boolean.TRUE.equals(property.isFile)) { parameter.isFile = true; } if (Boolean.TRUE.equals(property.isModel)) { parameter.isModel = true; } } /** * Update codegen property's enum by adding "enumVars" (with name and value) * * @param var list of CodegenProperty */ public void updateCodegenPropertyEnum(CodegenProperty var) { Map allowableValues = var.allowableValues; // handle array if (var.mostInnerItems != null) { allowableValues = var.mostInnerItems.allowableValues; } if (allowableValues == null) { return; } List values = (List) allowableValues.get("values"); if (values == null) { return; } String varDataType = var.mostInnerItems != null ? var.mostInnerItems.dataType : var.dataType; Optional referencedSchema = ModelUtils.getSchemas(openAPI).entrySet().stream() .filter(entry -> Objects.equals(varDataType, toModelName(entry.getKey()))) .map(Map.Entry::getValue) .findFirst(); String dataType = (referencedSchema.isPresent()) ? getTypeDeclaration(referencedSchema.get()) : varDataType; List> enumVars = buildEnumVars(values, dataType); postProcessEnumVars(enumVars); // if "x-enum-varnames" or "x-enum-descriptions" defined, update varnames Map extensions = var.mostInnerItems != null ? var.mostInnerItems.getVendorExtensions() : var.getVendorExtensions(); if (referencedSchema.isPresent()) { extensions = referencedSchema.get().getExtensions(); } updateEnumVarsWithExtensions(enumVars, extensions, dataType); allowableValues.put("enumVars", enumVars); // handle default value for enum, e.g. available => StatusEnum.AVAILABLE if (var.defaultValue != null) { final String enumDefaultValue = getEnumDefaultValue(var.defaultValue, dataType); String enumName = null; for (Map enumVar : enumVars) { if (enumDefaultValue.equals(enumVar.get("value"))) { enumName = (String) enumVar.get("name"); break; } } if (enumName != null) { var.defaultValue = toEnumDefaultValue(enumName, var.datatypeWithEnum); } } } protected String getEnumDefaultValue(String defaultValue, String dataType) { final String enumDefaultValue; if (isDataTypeString(dataType)) { enumDefaultValue = toEnumValue(defaultValue, dataType); } else { enumDefaultValue = defaultValue; } return enumDefaultValue; } protected List> buildEnumVars(List values, String dataType) { List> enumVars = new ArrayList<>(); int truncateIdx = isRemoveEnumValuePrefix() ? findCommonPrefixOfVars(values).length() : 0; for (Object value : values) { Map enumVar = new HashMap<>(); String enumName = truncateIdx == 0 ? String.valueOf(value) : value.toString().substring(truncateIdx); if (enumName.isEmpty()) { enumName = value.toString(); } final String finalEnumName = toEnumVarName(enumName, dataType); enumVar.put("name", finalEnumName); enumVar.put("value", toEnumValue(String.valueOf(value), dataType)); enumVar.put("isString", isDataTypeString(dataType)); enumVars.add(enumVar); } if (enumUnknownDefaultCase) { // If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. // With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case. Map enumVar = new HashMap<>(); String enumName = enumUnknownDefaultCaseName; String enumValue = isDataTypeString(dataType) ? enumUnknownDefaultCaseName : // This is a dummy value that attempts to avoid collisions with previously specified cases. // Int.max / 192 // The number 192 that is used to calculate this random value, is the Swift Evolution proposal for frozen/non-frozen enums. // [SE-0192](https://github.com/apple/swift-evolution/blob/master/proposals/0192-non-exhaustive-enums.md) // Since this functionality was born in the Swift 5 generator and latter on broth to all generators // https://github.com/OpenAPITools/openapi-generator/pull/11013 String.valueOf(11184809); enumVar.put("name", toEnumVarName(enumName, dataType)); enumVar.put("value", toEnumValue(enumValue, dataType)); enumVar.put("isString", isDataTypeString(dataType)); enumVars.add(enumVar); } return enumVars; } protected void postProcessEnumVars(List> enumVars) { Collections.reverse(enumVars); enumVars.forEach(v -> { String name = (String) v.get("name"); 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); v.put("name", uniqueEnumName); } }); Collections.reverse(enumVars); } private String getUniqueEnumName(String name, List> enumVars) { long count = enumVars.stream().filter(v -> v.get("name").equals(name)).count(); return count > 1 ? getUniqueEnumName(name + count, enumVars) : name; } protected void updateEnumVarsWithExtensions(List> enumVars, Map vendorExtensions, String dataType) { if (vendorExtensions != null) { updateEnumVarsWithExtensions(enumVars, vendorExtensions, "x-enum-varnames", "name"); updateEnumVarsWithExtensions(enumVars, vendorExtensions, "x-enum-descriptions", "enumDescription"); } } private void updateEnumVarsWithExtensions(List> enumVars, Map vendorExtensions, String extensionKey, String key) { if (vendorExtensions.containsKey(extensionKey)) { List values = (List) vendorExtensions.get(extensionKey); int size = Math.min(enumVars.size(), values.size()); for (int i = 0; i < size; i++) { enumVars.get(i).put(key, values.get(i)); } } } /** * If the pattern misses the delimiter, add "/" to the beginning and end * Otherwise, return the original pattern * * @param pattern the pattern (regular expression) * @return the pattern with delimiter */ public String addRegularExpressionDelimiter(String pattern) { if (StringUtils.isEmpty(pattern)) { return pattern; } if (!pattern.matches("^/.*")) { return "/" + pattern.replaceAll("/", "\\\\/") + "/"; } return pattern; } /** * reads propertyKey from additionalProperties, converts it to a boolean and * writes it back to additionalProperties to be usable as a boolean in * mustache files. * * @param propertyKey property key * @return property value as boolean */ public boolean convertPropertyToBooleanAndWriteBack(String propertyKey) { boolean result = convertPropertyToBoolean(propertyKey); writePropertyBack(propertyKey, result); return result; } /** * Provides an override location, if any is specified, for the .openapi-generator-ignore. *

* This is originally intended for the first generation only. * * @return a string of the full path to an override ignore file. */ @Override public String getIgnoreFilePathOverride() { return ignoreFilePathOverride; } /** * Sets an override location for the '.openapi-generator-ignore' location for the first code generation. * * @param ignoreFileOverride The full path to an ignore file */ @Override public void setIgnoreFilePathOverride(final String ignoreFileOverride) { this.ignoreFilePathOverride = ignoreFileOverride; } public boolean convertPropertyToBoolean(String propertyKey) { final Object booleanValue = additionalProperties.get(propertyKey); boolean result = Boolean.FALSE; if (booleanValue instanceof Boolean) { result = (Boolean) booleanValue; } else if (booleanValue instanceof String) { result = Boolean.parseBoolean((String) booleanValue); } else { LOGGER.warn("The value (generator's option) must be either boolean or string. Default to `false`."); } return result; } public void writePropertyBack(String propertyKey, Object value) { additionalProperties.put(propertyKey, value); } protected String getContentType(RequestBody requestBody) { if (requestBody == null || requestBody.getContent() == null || requestBody.getContent().isEmpty()) { LOGGER.debug("Cannot determine the content type. Returning null."); return null; } return new ArrayList<>(requestBody.getContent().keySet()).get(0); } private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) { codegenSecurity.authorizationUrl = flow.getAuthorizationUrl(); codegenSecurity.tokenUrl = flow.getTokenUrl(); codegenSecurity.refreshUrl = flow.getRefreshUrl(); if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List> scopes = new ArrayList<>(); for (Map.Entry scopeEntry : flow.getScopes().entrySet()) { Map scope = new HashMap<>(); scope.put("scope", scopeEntry.getKey()); scope.put("description", escapeText(scopeEntry.getValue())); scopes.add(scope); } codegenSecurity.scopes = scopes; } } private void setOpenIdConnectInfo(CodegenSecurity codegenSecurity, OAuthFlow flow) { if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List> scopes = new ArrayList<>(); for (Map.Entry scopeEntry : flow.getScopes().entrySet()) { Map scope = new HashMap<>(); scope.put("scope", scopeEntry.getKey()); scopes.add(scope); } codegenSecurity.scopes = scopes; } } private void addConsumesInfo(Operation operation, CodegenOperation codegenOperation) { RequestBody requestBody = ModelUtils.getReferencedRequestBody(this.openAPI, operation.getRequestBody()); if (requestBody == null || requestBody.getContent() == null || requestBody.getContent().isEmpty()) { return; } Set consumes = requestBody.getContent().keySet(); List> mediaTypeList = new ArrayList<>(); for (String key : consumes) { Map mediaType = new HashMap<>(); if ("*/*".equals(key)) { // skip as it implies `consumes` in OAS2 is not defined continue; } else { mediaType.put("mediaType", escapeQuotationMark(key)); if (isJsonMimeType(key)) { mediaType.put("isJson", "true"); } else if (isXmlMimeType(key)) { mediaType.put("isXml", "true"); } } mediaTypeList.add(mediaType); } if (!mediaTypeList.isEmpty()) { codegenOperation.consumes = mediaTypeList; codegenOperation.hasConsumes = true; } } public static Set getConsumesInfo(OpenAPI openAPI, Operation operation) { RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, operation.getRequestBody()); if (requestBody == null || requestBody.getContent() == null || requestBody.getContent().isEmpty()) { return Collections.emptySet(); // return empty set } return requestBody.getContent().keySet(); } public boolean hasFormParameter(Operation operation) { Set consumesInfo = getConsumesInfo(openAPI, operation); if (consumesInfo == null || consumesInfo.isEmpty()) { return false; } for (String consume : consumesInfo) { if (consume != null && (consume.toLowerCase(Locale.ROOT).startsWith("application/x-www-form-urlencoded") || consume.toLowerCase(Locale.ROOT).startsWith("multipart"))) { return true; } } return false; } public boolean hasBodyParameter(Operation operation) { RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, operation.getRequestBody()); if (requestBody == null) { return false; } Schema schema = ModelUtils.getSchemaFromRequestBody(requestBody); return ModelUtils.getReferencedSchema(openAPI, schema) != null; } private void addProducesInfo(ApiResponse inputResponse, CodegenOperation codegenOperation) { ApiResponse response = ModelUtils.getReferencedApiResponse(this.openAPI, inputResponse); if (response == null || response.getContent() == null || response.getContent().isEmpty()) { return; } Set produces = response.getContent().keySet(); if (codegenOperation.produces == null) { codegenOperation.produces = new ArrayList<>(); } Set existingMediaTypes = new HashSet<>(); for (Map mediaType : codegenOperation.produces) { existingMediaTypes.add(mediaType.get("mediaType")); } for (String key : produces) { // escape quotation to avoid code injection, "*/*" is a special case, do nothing String encodedKey = "*/*".equals(key) ? key : escapeQuotationMark(key); //Only unique media types should be added to "produces" if (!existingMediaTypes.contains(encodedKey)) { Map mediaType = new HashMap<>(); mediaType.put("mediaType", encodedKey); if (isJsonMimeType(encodedKey)) { mediaType.put("isJson", "true"); } else if (isXmlMimeType(encodedKey)) { mediaType.put("isXml", "true"); } codegenOperation.produces.add(mediaType); codegenOperation.hasProduces = Boolean.TRUE; } } } /** * returns the list of MIME types the APIs can produce * * @param openAPI current specification instance * @param operation Operation * @return a set of MIME types */ public static Set getProducesInfo(final OpenAPI openAPI, final Operation operation) { if (operation.getResponses() == null || operation.getResponses().isEmpty()) { return null; } Set produces = new ConcurrentSkipListSet<>(); for (ApiResponse r : operation.getResponses().values()) { ApiResponse response = ModelUtils.getReferencedApiResponse(openAPI, r); if (response.getContent() != null) { produces.addAll(response.getContent().keySet()); } } return produces; } protected String getCollectionFormat(Parameter parameter) { if (Parameter.StyleEnum.FORM.equals(parameter.getStyle())) { // Ref: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#style-values if (Boolean.TRUE.equals(parameter.getExplode())) { // explode is true (default) return "multi"; } else { return "csv"; } } else if (Parameter.StyleEnum.SIMPLE.equals(parameter.getStyle())) { return "csv"; } else if (Parameter.StyleEnum.PIPEDELIMITED.equals(parameter.getStyle())) { return "pipes"; } else if (Parameter.StyleEnum.SPACEDELIMITED.equals(parameter.getStyle())) { return "ssv"; } else { return null; } } @Override public CodegenType getTag() { return null; } @Override public String getName() { return null; } @Override public String getHelp() { return null; } public List fromRequestBodyToFormParameters(RequestBody body, Set imports) { List parameters = new ArrayList<>(); LOGGER.debug("debugging fromRequestBodyToFormParameters= {}", body); Schema schema = ModelUtils.getSchemaFromRequestBody(body); schema = ModelUtils.getReferencedSchema(this.openAPI, schema); if (ModelUtils.isMapSchema(schema)) { LOGGER.error("Form parameters with additionalProperties are not supported by OpenAPI Generator. Please report the issue to https://github.com/openapitools/openapi-generator if you need help."); } if (ModelUtils.isArraySchema(schema)) { LOGGER.error("Array form parameters are not supported by OpenAPI Generator. Please report the issue to https://github.com/openapitools/openapi-generator if you need help."); } List allRequired = new ArrayList<>(); Map properties = new LinkedHashMap<>(); // this traverses a composed schema and extracts all properties in each schema into properties // TODO in the future have this return one codegenParameter of type object or composed which includes all definition // that will be needed for complex composition use cases // https://github.com/OpenAPITools/openapi-generator/issues/10415 addProperties(properties, allRequired, schema, new HashSet<>()); boolean isOneOfOrAnyOf = ModelUtils.isOneOf(schema) || ModelUtils.isAnyOf(schema); if (!properties.isEmpty()) { for (Map.Entry entry : properties.entrySet()) { CodegenParameter codegenParameter; // key => property name // value => property schema String propertyName = entry.getKey(); Schema propertySchema = entry.getValue(); codegenParameter = fromFormProperty(propertyName, propertySchema, imports); if (isOneOfOrAnyOf) { // for oneOf/anyOf, mark all the properties collected from the sub-schemas as optional // so that users can choose which property to include in the form parameters codegenParameter.required = false; } else if (!codegenParameter.required && schema.getRequired() != null) { // Set 'required' flag defined in the schema element codegenParameter.required = schema.getRequired().contains(entry.getKey()); } else if (!codegenParameter.required) { // Set 'required' flag for properties declared inside the allOf codegenParameter.required = allRequired.stream().anyMatch(r -> r.equals(codegenParameter.paramName)); } parameters.add(codegenParameter); } } return parameters; } public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set imports) { CodegenParameter codegenParameter = CodegenModelFactory.newInstance(CodegenModelType.PARAMETER); LOGGER.debug("Debugging fromFormProperty {}: {}", name, propertySchema); CodegenProperty codegenProperty = fromProperty(name, propertySchema, false); Schema ps = unaliasSchema(propertySchema); ModelUtils.syncValidationProperties(ps, codegenParameter); codegenParameter.setTypeProperties(ps); codegenParameter.setComposedSchemas(getComposedSchemas(ps)); if (ps.getPattern() != null) { codegenParameter.pattern = toRegularExpression(ps.getPattern()); } codegenParameter.baseType = codegenProperty.baseType; codegenParameter.dataType = codegenProperty.dataType; codegenParameter.baseName = codegenProperty.baseName; codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.dataFormat = codegenProperty.dataFormat; // non-array/map updateCodegenPropertyEnum(codegenProperty); codegenParameter.isEnum = codegenProperty.isEnum; codegenParameter.isEnumRef = codegenProperty.isEnumRef; codegenParameter._enum = codegenProperty._enum; codegenParameter.allowableValues = codegenProperty.allowableValues; // set default value codegenParameter.defaultValue = toDefaultParameterValue(codegenProperty, propertySchema); if (ModelUtils.isFileSchema(ps) && !ModelUtils.isStringSchema(ps)) { // swagger v2 only, type file codegenParameter.isFile = true; } else if (ModelUtils.isStringSchema(ps)) { if (ModelUtils.isEmailSchema(ps)) { codegenParameter.isEmail = true; } else if (ModelUtils.isPasswordSchema(ps)) { codegenParameter.isPassword = true; } else if (ModelUtils.isUUIDSchema(ps)) { codegenParameter.isUuid = true; } else if (ModelUtils.isByteArraySchema(ps)) { codegenParameter.setIsString(false); codegenParameter.isByteArray = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isBinarySchema(ps)) { codegenParameter.isBinary = true; codegenParameter.isFile = true; // file = binary in OAS3 codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDateSchema(ps)) { codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDate = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDateTimeSchema(ps)) { codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDateTime = true; codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isDecimalSchema(ps)) { // type: string, format: number codegenParameter.setIsString(false); codegenParameter.isDecimal = true; codegenParameter.isPrimitiveType = true; } if (Boolean.TRUE.equals(codegenParameter.isString)) { codegenParameter.isPrimitiveType = true; } } else if (ModelUtils.isBooleanSchema(ps)) { codegenParameter.isPrimitiveType = true; } else if (ModelUtils.isNumberSchema(ps)) { codegenParameter.isPrimitiveType = true; if (ModelUtils.isFloatSchema(ps)) { // float codegenParameter.isFloat = true; } else if (ModelUtils.isDoubleSchema(ps)) { // double codegenParameter.isDouble = true; } } else if (ModelUtils.isIntegerSchema(ps)) { // integer type codegenParameter.isPrimitiveType = true; if (ModelUtils.isLongSchema(ps)) { // int64/long format codegenParameter.isLong = true; } else { codegenParameter.isInteger = true; if (ModelUtils.isShortSchema(ps)) { // int32/short format codegenParameter.isShort = true; } else { // unbounded integer } } } else if (ModelUtils.isTypeObjectSchema(ps)) { if (ModelUtils.isFreeFormObject(ps)) { codegenParameter.isFreeFormObject = true; } } else if (ModelUtils.isNullType(ps)) { } else if (ModelUtils.isAnyType(ps)) { // any schema with no type set, composed schemas often do this } else if (ModelUtils.isArraySchema(ps)) { Schema inner = getSchemaItems((ArraySchema) ps); CodegenProperty arrayInnerProperty = fromProperty("inner", inner, false); codegenParameter.items = arrayInnerProperty; codegenParameter.mostInnerItems = arrayInnerProperty.mostInnerItems; codegenParameter.isPrimitiveType = false; codegenParameter.isContainer = true; // hoist items data into the array property // TODO this hoisting code is generator specific and should be isolated into updateFormPropertyForArray codegenParameter.baseType = arrayInnerProperty.dataType; // TODO we need to fix array of item (with default value) generator by generator // https://github.com/OpenAPITools/openapi-generator/pull/16654/ is a good reference if (!(this instanceof PhpNextgenClientCodegen)) { // no need to set default value here as it was set earlier codegenParameter.defaultValue = arrayInnerProperty.getDefaultValue(); } if (codegenParameter.items.isFile) { codegenParameter.isFile = true; codegenParameter.dataFormat = codegenParameter.items.dataFormat; } if (arrayInnerProperty._enum != null) { codegenParameter._enum = arrayInnerProperty._enum; } if (arrayInnerProperty.baseType != null && arrayInnerProperty.enumName != null) { codegenParameter.datatypeWithEnum = codegenParameter.dataType.replace(arrayInnerProperty.baseType, arrayInnerProperty.enumName); } else { LOGGER.warn("Could not compute datatypeWithEnum from {}, {}", arrayInnerProperty.baseType, arrayInnerProperty.enumName); } // end of hoisting // collectionFormat for form parameter does not consider // style and explode from encoding at this point String collectionFormat = getCollectionFormat(codegenParameter); codegenParameter.collectionFormat = StringUtils.isEmpty(collectionFormat) ? "csv" : collectionFormat; codegenParameter.isCollectionFormatMulti = "multi".equals(collectionFormat); // recursively add import while (arrayInnerProperty != null) { imports.add(arrayInnerProperty.baseType); arrayInnerProperty = arrayInnerProperty.items; } } else { // referenced schemas } if (Boolean.TRUE.equals(codegenProperty.isModel)) { codegenParameter.isModel = true; } codegenParameter.isFormParam = Boolean.TRUE; codegenParameter.description = escapeText(codegenProperty.description); codegenParameter.unescapedDescription = codegenProperty.getDescription(); codegenParameter.jsonSchema = Json.pretty(propertySchema); codegenParameter.containerType = codegenProperty.containerType; codegenParameter.containerTypeMapped = codegenProperty.containerTypeMapped; if (codegenProperty.getVendorExtensions() != null && !codegenProperty.getVendorExtensions().isEmpty()) { codegenParameter.vendorExtensions = codegenProperty.getVendorExtensions(); } if (propertySchema.getRequired() != null && !propertySchema.getRequired().isEmpty() && propertySchema.getRequired().contains(codegenProperty.baseName)) { codegenParameter.required = Boolean.TRUE; } if (codegenProperty.isEnum) { codegenParameter.datatypeWithEnum = codegenProperty.datatypeWithEnum; codegenParameter.enumName = codegenProperty.enumName; if (codegenProperty.defaultValue != null) { codegenParameter.enumDefaultValue = codegenProperty.defaultValue.replace(codegenProperty.enumName + ".", ""); } } // import if (codegenProperty.complexType != null) { imports.add(codegenProperty.complexType); } // set example value setParameterExampleValue(codegenParameter); // set nullable setParameterNullable(codegenParameter, codegenProperty); return codegenParameter; } protected void addBodyModelSchema(CodegenParameter codegenParameter, String name, Schema schema, Set imports, String bodyParameterName, boolean forceSimpleRef) { CodegenModel codegenModel = null; if (StringUtils.isNotBlank(name)) { schema.setName(name); codegenModel = fromModel(name, schema); } if (codegenModel != null) { codegenParameter.isModel = true; } if (codegenModel != null && (codegenModel.hasVars || forceSimpleRef)) { if (StringUtils.isEmpty(bodyParameterName)) { codegenParameter.baseName = codegenModel.classname; } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.baseType = codegenModel.classname; codegenParameter.dataType = getTypeDeclaration(codegenModel.classname); codegenParameter.description = codegenModel.description; codegenParameter.isNullable = codegenModel.isNullable; imports.add(codegenParameter.baseType); } else { CodegenProperty codegenProperty = fromProperty("property", schema, false); if (codegenProperty != null && codegenProperty.getComplexType() != null && codegenProperty.getComplexType().contains(" | ")) { // TODO move this splitting logic to the generator that needs it only List parts = Arrays.asList(codegenProperty.getComplexType().split(" \\| ")); imports.addAll(parts); String codegenModelName = codegenProperty.getComplexType(); codegenParameter.baseName = codegenModelName; codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.baseType = codegenParameter.baseName; codegenParameter.dataType = getTypeDeclaration(codegenModelName); codegenParameter.description = codegenProperty.getDescription(); codegenParameter.isNullable = codegenProperty.isNullable; } else { if (ModelUtils.isMapSchema(schema)) {// http body is map LOGGER.error("Map should be supported. Please report to openapi-generator github repo about the issue."); } else if (codegenProperty != null) { String codegenModelName, codegenModelDescription; if (codegenModel != null) { codegenModelName = codegenModel.classname; 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/?'"); LOGGER.warn("schema: {}", schema); LOGGER.warn("codegenModel is null. Default to UNKNOWN_BASE_TYPE"); codegenModelName = "UNKNOWN_BASE_TYPE"; codegenModelDescription = "UNKNOWN_DESCRIPTION"; } if (StringUtils.isEmpty(bodyParameterName)) { codegenParameter.baseName = codegenModelName; } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.baseType = codegenModelName; codegenParameter.dataType = getTypeDeclaration(codegenModelName); codegenParameter.description = codegenModelDescription; imports.add(codegenParameter.baseType); if (codegenProperty.complexType != null && codegenProperty.getComposedSchemas() == null) { imports.add(codegenProperty.complexType); } } } // set nullable setParameterNullable(codegenParameter, codegenProperty); } } protected void updateRequestBodyForMap(CodegenParameter codegenParameter, Schema schema, String name, Set imports, String bodyParameterName) { if (StringUtils.isNotBlank(name)) { this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, true); } else { Schema inner = ModelUtils.getAdditionalProperties(schema); if (inner == null) { LOGGER.error("No inner type supplied for map parameter `{}`. Default to type:string", schema.getName()); inner = new StringSchema().description("//TODO automatically added by openapi-generator"); schema.setAdditionalProperties(inner); } CodegenProperty codegenProperty = fromProperty("property", schema, false); imports.add(codegenProperty.baseType); CodegenProperty innerCp = codegenProperty; while (innerCp != null) { if (innerCp.complexType != null) { imports.add(innerCp.complexType); } innerCp = innerCp.items; } if (StringUtils.isEmpty(bodyParameterName)) { codegenParameter.baseName = "request_body"; } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.items = codegenProperty.items; codegenParameter.mostInnerItems = codegenProperty.mostInnerItems; codegenParameter.dataType = getTypeDeclaration(schema); codegenParameter.baseType = getSchemaType(inner); codegenParameter.isContainer = Boolean.TRUE; codegenParameter.isMap = Boolean.TRUE; codegenParameter.isNullable = codegenProperty.isNullable; codegenParameter.containerType = codegenProperty.containerType; codegenParameter.containerTypeMapped = codegenProperty.containerTypeMapped; // set nullable setParameterNullable(codegenParameter, codegenProperty); } } protected void updateRequestBodyForPrimitiveType(CodegenParameter codegenParameter, Schema schema, String bodyParameterName, Set imports) { CodegenProperty codegenProperty = fromProperty("PRIMITIVE_REQUEST_BODY", schema, false); if (codegenProperty != null) { if (StringUtils.isEmpty(bodyParameterName)) { codegenParameter.baseName = "body"; // default to body } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.isPrimitiveType = true; codegenParameter.baseType = codegenProperty.baseType; codegenParameter.dataType = codegenProperty.dataType; codegenParameter.description = codegenProperty.description; codegenParameter.paramName = toParamName(codegenParameter.baseName); codegenParameter.pattern = codegenProperty.pattern; codegenParameter.isNullable = codegenProperty.isNullable; if (codegenProperty.complexType != null) { imports.add(codegenProperty.complexType); } } // set nullable setParameterNullable(codegenParameter, codegenProperty); } protected void updateRequestBodyForObject(CodegenParameter codegenParameter, Schema schema, String name, Set imports, String bodyParameterName) { if (ModelUtils.isMapSchema(schema)) { // Schema with additionalproperties: true (including composed schemas with additionalproperties: true) updateRequestBodyForMap(codegenParameter, schema, name, imports, bodyParameterName); } else if (ModelUtils.isFreeFormObject(schema)) { // non-composed object type with no properties + additionalProperties // additionalProperties must be null, ObjectSchema, or empty Schema codegenParameter.isFreeFormObject = true; // HTTP request body is free form object CodegenProperty codegenProperty = fromProperty("FREE_FORM_REQUEST_BODY", schema, false); if (codegenProperty != null) { if (StringUtils.isEmpty(bodyParameterName)) { codegenParameter.baseName = "body"; // default to body } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.isPrimitiveType = true; codegenParameter.baseType = codegenProperty.baseType; codegenParameter.dataType = codegenProperty.dataType; codegenParameter.description = codegenProperty.description; codegenParameter.isNullable = codegenProperty.isNullable; codegenParameter.paramName = toParamName(codegenParameter.baseName); } // set nullable setParameterNullable(codegenParameter, codegenProperty); } else if (ModelUtils.isObjectSchema(schema)) { // object type schema or composed schema with properties defined this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, false); } addVarsRequiredVarsAdditionalProps(schema, codegenParameter); } protected void updateRequestBodyForArray(CodegenParameter codegenParameter, Schema schema, String name, Set imports, String bodyParameterName) { if (ModelUtils.isGenerateAliasAsModel(schema) && StringUtils.isNotBlank(name)) { this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, true); } else { final ArraySchema arraySchema = (ArraySchema) schema; Schema inner = getSchemaItems(arraySchema); CodegenProperty codegenProperty = fromProperty("property", arraySchema, false); if (codegenProperty == null) { throw new RuntimeException("CodegenProperty cannot be null. arraySchema for debugging: " + arraySchema); } imports.add(codegenProperty.baseType); CodegenProperty innerCp = codegenProperty; CodegenProperty mostInnerItem = innerCp; // loop through multidimensional array to add proper import // also find the most inner item while (innerCp != null) { if (innerCp.complexType != null) { imports.add(innerCp.complexType); } mostInnerItem = innerCp; innerCp = innerCp.items; } if (StringUtils.isEmpty(bodyParameterName)) { if (StringUtils.isEmpty(mostInnerItem.complexType)) { codegenParameter.baseName = "request_body"; } else { codegenParameter.baseName = mostInnerItem.complexType; } } else { codegenParameter.baseName = bodyParameterName; } codegenParameter.paramName = toArrayModelParamName(codegenParameter.baseName); codegenParameter.items = codegenProperty.items; codegenParameter.mostInnerItems = codegenProperty.mostInnerItems; codegenParameter.dataType = getTypeDeclaration(arraySchema); codegenParameter.baseType = getSchemaType(inner); codegenParameter.isContainer = Boolean.TRUE; codegenParameter.isNullable = codegenProperty.isNullable; codegenParameter.containerType = codegenProperty.containerType; codegenParameter.containerTypeMapped = codegenProperty.containerTypeMapped; // set nullable setParameterNullable(codegenParameter, codegenProperty); while (codegenProperty != null) { imports.add(codegenProperty.baseType); codegenProperty = codegenProperty.items; } } } protected void updateRequestBodyForString(CodegenParameter codegenParameter, Schema schema, Set imports, String bodyParameterName) { updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); if (ModelUtils.isByteArraySchema(schema)) { codegenParameter.setIsString(false); codegenParameter.isByteArray = true; } else if (ModelUtils.isBinarySchema(schema)) { codegenParameter.isBinary = true; codegenParameter.isFile = true; // file = binary in OAS3 } else if (ModelUtils.isUUIDSchema(schema)) { codegenParameter.isUuid = true; } else if (ModelUtils.isURISchema(schema)) { codegenParameter.isUri = true; } else if (ModelUtils.isEmailSchema(schema)) { codegenParameter.isEmail = true; } else if (ModelUtils.isDateSchema(schema)) { // date format codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDate = true; } else if (ModelUtils.isDateTimeSchema(schema)) { // date-time format codegenParameter.setIsString(false); // for backward compatibility with 2.x codegenParameter.isDateTime = true; } else if (ModelUtils.isDecimalSchema(schema)) { // type: string, format: number codegenParameter.isDecimal = true; codegenParameter.setIsString(false); } codegenParameter.pattern = toRegularExpression(schema.getPattern()); } protected String toMediaTypeSchemaName(String contentType, String mediaTypeSchemaSuffix) { return "SchemaFor" + mediaTypeSchemaSuffix + toModelName(contentType); } private CodegenParameter headerToCodegenParameter(Header header, String headerName, Set imports, String mediaTypeSchemaSuffix) { if (header == null) { return null; } Parameter headerParam = new Parameter(); headerParam.setName(headerName); headerParam.setIn("header"); headerParam.setDescription(header.getDescription()); headerParam.setRequired(header.getRequired()); headerParam.setDeprecated(header.getDeprecated()); Header.StyleEnum style = header.getStyle(); if (style != null) { headerParam.setStyle(Parameter.StyleEnum.valueOf(style.name())); } headerParam.setExplode(header.getExplode()); headerParam.setSchema(header.getSchema()); headerParam.setExamples(header.getExamples()); headerParam.setExample(header.getExample()); headerParam.setContent(header.getContent()); headerParam.setExtensions(header.getExtensions()); CodegenParameter param = fromParameter(headerParam, imports); param.setContent(getContent(headerParam.getContent(), imports, mediaTypeSchemaSuffix)); return param; } protected LinkedHashMap getContent(Content content, Set imports, String mediaTypeSchemaSuffix) { if (content == null) { return null; } LinkedHashMap cmtContent = new LinkedHashMap<>(); for (Entry contentEntry : content.entrySet()) { MediaType mt = contentEntry.getValue(); LinkedHashMap ceMap = null; if (mt.getEncoding() != null) { ceMap = new LinkedHashMap<>(); Map encMap = mt.getEncoding(); for (Entry encodingEntry : encMap.entrySet()) { Encoding enc = encodingEntry.getValue(); List headers = new ArrayList<>(); if (enc.getHeaders() != null) { Map encHeaders = enc.getHeaders(); for (Entry headerEntry : encHeaders.entrySet()) { String headerName = headerEntry.getKey(); Header header = ModelUtils.getReferencedHeader(this.openAPI, headerEntry.getValue()); CodegenParameter param = headerToCodegenParameter(header, headerName, imports, mediaTypeSchemaSuffix); headers.add(param); } } CodegenEncoding ce = new CodegenEncoding( enc.getContentType(), headers, enc.getStyle().toString(), enc.getExplode() == null ? false : enc.getExplode().booleanValue(), enc.getAllowReserved() == null ? false : enc.getAllowReserved().booleanValue() ); if (enc.getExtensions() != null) { ce.vendorExtensions = enc.getExtensions(); } String propName = encodingEntry.getKey(); ceMap.put(propName, ce); } } String contentType = contentEntry.getKey(); CodegenProperty schemaProp = null; if (mt.getSchema() != null) { schemaProp = fromProperty(toMediaTypeSchemaName(contentType, mediaTypeSchemaSuffix), mt.getSchema(), false); } HashMap schemaTestCases = null; if (mt.getExtensions() != null && mt.getExtensions().containsKey(xSchemaTestExamplesKey)) { Object objNodeWithRef = mt.getExtensions().get(xSchemaTestExamplesKey); if (objNodeWithRef instanceof LinkedHashMap) { LinkedHashMap nodeWithRef = (LinkedHashMap) objNodeWithRef; String refKey = "$ref"; String refToTestCases = nodeWithRef.getOrDefault(refKey, null); if (refToTestCases != null) { schemaTestCases = extractSchemaTestCases(refToTestCases); } } } CodegenMediaType codegenMt; if (mt.getExamples() != null) { codegenMt = new CodegenMediaType(schemaProp, ceMap, schemaTestCases, mt.getExamples()); } else if (mt.getExample() != null) { codegenMt = new CodegenMediaType(schemaProp, ceMap, schemaTestCases, mt.getExample()); } else { codegenMt = new CodegenMediaType(schemaProp, ceMap, schemaTestCases); } if (mt.getExtensions() != null) { codegenMt.vendorExtensions = mt.getExtensions(); } cmtContent.put(contentType, codegenMt); if (schemaProp != null) { addImports(imports, schemaProp.getImports(true, importBaseType, generatorMetadata.getFeatureSet())); } } return cmtContent; } public CodegenParameter fromRequestBody(RequestBody body, Set imports, String bodyParameterName) { if (body == null) { LOGGER.error("body in fromRequestBody cannot be null!"); throw new RuntimeException("body in fromRequestBody cannot be null!"); } CodegenParameter codegenParameter = CodegenModelFactory.newInstance(CodegenModelType.PARAMETER); codegenParameter.baseName = "UNKNOWN_BASE_NAME"; codegenParameter.paramName = "UNKNOWN_PARAM_NAME"; codegenParameter.description = escapeText(body.getDescription()); codegenParameter.required = body.getRequired() != null ? body.getRequired() : Boolean.FALSE; codegenParameter.isBodyParam = Boolean.TRUE; if (body.getExtensions() != null) { codegenParameter.vendorExtensions.putAll(body.getExtensions()); } String name = null; LOGGER.debug("Request body = {}", body); Schema schema = ModelUtils.getSchemaFromRequestBody(body); if (schema == null) { LOGGER.error("Schema cannot be null in the request body: {}", body); return null; } 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 && schema.getType() == null && schema.getTypes() == null) { if (schema.getAllOf().get(0) instanceof Schema) { original = schema; schema = (Schema) schema.getAllOf().get(0); } else { LOGGER.error("Unknown type in allOf schema. Please report the issue via openapi-generator's Github issue tracker."); } } codegenParameter.setContent(getContent(body.getContent(), imports, "RequestBody")); if (StringUtils.isNotBlank(schema.get$ref())) { name = ModelUtils.getSimpleRef(schema.get$ref()); } Schema unaliasedSchema = unaliasSchema(schema); schema = ModelUtils.getReferencedSchema(this.openAPI, schema); ModelUtils.syncValidationProperties(unaliasedSchema, codegenParameter); codegenParameter.setTypeProperties(unaliasedSchema); codegenParameter.setComposedSchemas(getComposedSchemas(unaliasedSchema)); // TODO in the future switch al the below schema usages to unaliasedSchema // because it keeps models as refs and will not get their referenced schemas if (ModelUtils.isArraySchema(schema)) { updateRequestBodyForArray(codegenParameter, schema, name, imports, bodyParameterName); } else if (ModelUtils.isTypeObjectSchema(schema)) { updateRequestBodyForObject(codegenParameter, schema, name, imports, bodyParameterName); } else if (ModelUtils.isIntegerSchema(schema)) { // integer type updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); codegenParameter.isNumeric = Boolean.TRUE; if (ModelUtils.isLongSchema(schema)) { // int64/long format codegenParameter.isLong = Boolean.TRUE; } else { codegenParameter.isInteger = Boolean.TRUE; // older use case, int32 and unbounded int if (ModelUtils.isShortSchema(schema)) { // int32 codegenParameter.setIsShort(Boolean.TRUE); } } } else if (ModelUtils.isBooleanSchema(schema)) { // boolean type updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); } else if (ModelUtils.isFileSchema(schema) && !ModelUtils.isStringSchema(schema)) { // swagger v2 only, type file codegenParameter.isFile = true; } else if (ModelUtils.isStringSchema(schema)) { updateRequestBodyForString(codegenParameter, schema, imports, bodyParameterName); } else if (ModelUtils.isNumberSchema(schema)) { updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); codegenParameter.isNumeric = Boolean.TRUE; if (ModelUtils.isFloatSchema(schema)) { // float codegenParameter.isFloat = Boolean.TRUE; } else if (ModelUtils.isDoubleSchema(schema)) { // double codegenParameter.isDouble = Boolean.TRUE; } } else if (ModelUtils.isNullType(schema)) { updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); } else if (ModelUtils.isAnyType(schema)) { if (ModelUtils.isMapSchema(schema)) { // Schema with additionalproperties: true (including composed schemas with additionalproperties: true) updateRequestBodyForMap(codegenParameter, schema, name, imports, bodyParameterName); } else if (ModelUtils.isComposedSchema(schema)) { this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, false); } else if (ModelUtils.isObjectSchema(schema)) { // object type schema OR (AnyType schema with properties defined) this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, false); } else { updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); } addVarsRequiredVarsAdditionalProps(schema, codegenParameter); } else { // referenced schemas updateRequestBodyForPrimitiveType(codegenParameter, schema, bodyParameterName, imports); } addJsonSchemaForBodyRequestInCaseItsNotPresent(codegenParameter, body); // set the parameter's example value // should be overridden by lang codegen setParameterExampleValue(codegenParameter, body); // restore original schema with description, extensions etc if (original != null) { schema = original; // evaluate common attributes such as description if defined in the top level if (schema.getDescription() != null) { codegenParameter.description = escapeText(schema.getDescription()); codegenParameter.unescapedDescription = schema.getDescription(); } if (original.getExtensions() != null) { codegenParameter.vendorExtensions.putAll(original.getExtensions()); } if (original.getDeprecated() != null) { codegenParameter.isDeprecated = original.getDeprecated(); } } return codegenParameter; } protected void addRequiredVarsMap(Schema schema, IJsonSchemaValidationProperties property) { /* this should be called after vars and additionalProperties are set Features added by storing codegenProperty values: - complexType stores reference to additionalProperties definition - baseName stores original name (can be invalid in a programming language) - nameInSnakeCase can store valid name for a programming language */ Map properties = schema.getProperties(); Map requiredVarsMap = new HashMap<>(); List requiredPropertyNames = schema.getRequired(); if (requiredPropertyNames == null) { return; } for (String requiredPropertyName : requiredPropertyNames) { // required property is defined in properties, value is that CodegenProperty String usedRequiredPropertyName = handleSpecialCharacters(requiredPropertyName); if (properties != null && properties.containsKey(requiredPropertyName)) { // get cp from property boolean found = false; for (CodegenProperty cp : property.getVars()) { if (cp.baseName.equals(requiredPropertyName)) { found = true; requiredVarsMap.put(requiredPropertyName, cp); break; } } if (found == false) { 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 // required property is not defined in properties, and additionalProperties is false, value is null requiredVarsMap.put(usedRequiredPropertyName, null); } else { // required property is not defined in properties, and additionalProperties is true or unset value is CodegenProperty made from empty schema // required property is not defined in properties, and additionalProperties is schema, value is CodegenProperty made from schema if (supportsAdditionalPropertiesWithComposedSchema && !disallowAdditionalPropertiesIfNotPresent) { CodegenProperty cp; if (schema.getAdditionalProperties() == null) { // additionalProperties is null cp = fromProperty(usedRequiredPropertyName, new Schema(), true, true); } else if (schema.getAdditionalProperties() instanceof Boolean && Boolean.TRUE.equals(schema.getAdditionalProperties())) { // additionalProperties is True cp = fromProperty(requiredPropertyName, new Schema(), true, true); } else { // additionalProperties is schema cp = fromProperty(requiredPropertyName, (Schema) schema.getAdditionalProperties(), true, true); } requiredVarsMap.put(usedRequiredPropertyName, cp); } } } if (!requiredVarsMap.isEmpty()) { property.setRequiredVarsMap(requiredVarsMap); } } protected void addVarsRequiredVarsAdditionalProps(Schema schema, IJsonSchemaValidationProperties property) { setAddProps(schema, property); Set mandatory = schema.getRequired() == null ? Collections.emptySet() : new TreeSet<>(schema.getRequired()); addVars(property, property.getVars(), schema.getProperties(), mandatory); addRequiredVarsMap(schema, property); } protected String getItemsName(Schema containingSchema, String containingSchemaName) { // fromProperty use case if (containingSchema.getExtensions() != null && containingSchema.getExtensions().get("x-item-name") != null) { return containingSchema.getExtensions().get("x-item-name").toString(); } return toVarName(containingSchemaName); } protected String getAdditionalPropertiesName() { return "additional_properties"; } private void addJsonSchemaForBodyRequestInCaseItsNotPresent(CodegenParameter codegenParameter, RequestBody body) { if (codegenParameter.jsonSchema == null) codegenParameter.jsonSchema = Json.pretty(body); } protected void addOption(String key, String description, String defaultValue) { addOption(key, description, defaultValue, null); } protected void addOption(String key, String description, String defaultValue, Map enumValues) { CliOption option = new CliOption(key, description); if (defaultValue != null) option.defaultValue(defaultValue); if (enumValues != null) option.setEnum(enumValues); cliOptions.add(option); } protected void updateOption(String key, String defaultValue) { for (CliOption cliOption : cliOptions) { if (cliOption.getOpt().equals(key)) { cliOption.setDefault(defaultValue); break; } } } protected void removeOption(String key) { for (int i = 0; i < cliOptions.size(); i++) { if (key.equals(cliOptions.get(i).getOpt())) { cliOptions.remove(i); break; } } } protected void addSwitch(String key, String description, Boolean defaultValue) { CliOption option = CliOption.newBoolean(key, description); if (defaultValue != null) option.defaultValue(defaultValue.toString()); cliOptions.add(option); } /** * generates OpenAPI specification file in JSON format * * @param objs map of object */ protected void generateJSONSpecFile(Map objs) { OpenAPI openAPI = (OpenAPI) objs.get("openAPI"); if (openAPI != null) { objs.put("openapi-json", SerializerUtils.toJsonString(openAPI)); } } /** * generates OpenAPI specification file in YAML format * * @param objs map of object */ public void generateYAMLSpecFile(Map objs) { OpenAPI openAPI = (OpenAPI) objs.get("openAPI"); String yaml = SerializerUtils.toYamlString(openAPI); if (yaml != null) { objs.put("openapi-yaml", yaml); } } /** * checks if the data should be classified as "string" in enum * e.g. double in C# needs to be double-quoted (e.g. "2.8") by treating it as a string * In the future, we may rename this function to "isEnumString" * * @param dataType data type * @return true if it's a enum string */ public boolean isDataTypeString(String dataType) { return "String".equals(dataType); } @Override public List fromServers(List servers) { if (servers == null) { return Collections.emptyList(); } List codegenServers = new LinkedList<>(); for (Server server : servers) { CodegenServer cs = new CodegenServer(); cs.description = escapeText(server.getDescription()); cs.url = server.getUrl(); cs.variables = this.fromServerVariables(server.getVariables()); if (server.getExtensions() != null) { cs.vendorExtensions = server.getExtensions(); } codegenServers.add(cs); } return codegenServers; } @Override public List fromServerVariables(Map variables) { if (variables == null) { return Collections.emptyList(); } Map variableOverrides = serverVariableOverrides(); List codegenServerVariables = new LinkedList<>(); for (Entry variableEntry : variables.entrySet()) { CodegenServerVariable codegenServerVariable = new CodegenServerVariable(); ServerVariable variable = variableEntry.getValue(); List enums = variable.getEnum(); codegenServerVariable.defaultValue = variable.getDefault(); codegenServerVariable.description = escapeText(variable.getDescription()); codegenServerVariable.enumValues = enums; codegenServerVariable.name = variableEntry.getKey(); if (variable.getExtensions() != null) { codegenServerVariable.vendorExtensions = variable.getExtensions(); } // Sets the override value for a server variable pattern. // NOTE: OpenAPI Specification doesn't prevent multiple server URLs with variables. If multiple objects have the same // variables pattern, user overrides will apply to _all_ of these patterns. We may want to consider indexed overrides. if (variableOverrides != null && !variableOverrides.isEmpty()) { String value = variableOverrides.getOrDefault(variableEntry.getKey(), variable.getDefault()); codegenServerVariable.value = value; if (enums != null && !enums.isEmpty() && !enums.contains(value)) { if (LOGGER.isWarnEnabled()) { // prevents calculating StringUtils.join when debug isn't enabled LOGGER.warn("Variable override of '{}' is not listed in the enum of allowed values ({}).", value, StringUtils.join(enums, ",")); } } } else { codegenServerVariable.value = variable.getDefault(); } codegenServerVariables.add(codegenServerVariable); } return codegenServerVariables; } protected void setParameterNullable(CodegenParameter parameter, CodegenProperty property) { if (parameter == null || property == null) { return; } parameter.isNullable = property.isNullable; } /** * Post-process the auto-generated file, e.g. using go-fmt to format the Go code. The file type can be "model-test", * "model-doc", "model", "api", "api-test", "api-doc", "supporting-mustache", "supporting-common", * "openapi-generator-ignore", "openapi-generator-version" *

* TODO: store these values in enum instead * * @param file file to be processed * @param fileType file type */ @Override public void postProcessFile(File file, String fileType) { LOGGER.debug("Post processing file {} ({})", file, fileType); } /** * Boolean value indicating the state of the option for post-processing file using environment variables. * * @return true if the option is enabled */ @Override public boolean isEnablePostProcessFile() { return enablePostProcessFile; } /** * Set the boolean value indicating the state of the option for post-processing file using environment variables. * * @param enablePostProcessFile true to enable post-processing file */ @Override public void setEnablePostProcessFile(boolean enablePostProcessFile) { this.enablePostProcessFile = enablePostProcessFile; } /** * Get the boolean value indicating the state of the option for updating only changed files */ @Override public boolean isEnableMinimalUpdate() { return enableMinimalUpdate; } /** * Set the boolean value indicating the state of the option for updating only changed files * * @param enableMinimalUpdate true to enable minimal update */ @Override public void setEnableMinimalUpdate(boolean enableMinimalUpdate) { this.enableMinimalUpdate = enableMinimalUpdate; } /** * Indicates whether the codegen configuration should treat documents as strictly defined by the OpenAPI specification. * * @return true to act strictly upon spec documents, potentially modifying the spec to strictly fit the spec. */ @Override public boolean isStrictSpecBehavior() { return this.strictSpecBehavior; } /** * Sets the boolean valid indicating whether generation will work strictly against the specification, potentially making * minor changes to the input document. * * @param strictSpecBehavior true if we will behave strictly, false to allow specification documents which pass validation to be loosely interpreted against the spec. */ @Override public void setStrictSpecBehavior(final boolean strictSpecBehavior) { this.strictSpecBehavior = strictSpecBehavior; } @Override public FeatureSet getFeatureSet() { return this.generatorMetadata.getFeatureSet(); } /** * Get the boolean value indicating whether to remove enum value prefixes */ @Override public boolean isRemoveEnumValuePrefix() { return this.removeEnumValuePrefix; } /** * Set the boolean value indicating whether to remove enum value prefixes * * @param removeEnumValuePrefix true to enable enum value prefix removal */ @Override public void setRemoveEnumValuePrefix(final boolean removeEnumValuePrefix) { this.removeEnumValuePrefix = removeEnumValuePrefix; } //// Following methods are related to the "useOneOfInterfaces" feature /** * Add "x-one-of-name" extension to a given oneOf schema (assuming it has at least 1 oneOf elements) * * @param schema schema to add the extension to * @param name name of the parent oneOf schema */ public void addOneOfNameExtension(Schema schema, String name) { if (schema.getOneOf() != null && schema.getOneOf().size() > 0) { schema.addExtension("x-one-of-name", name); } } /** * Add a given ComposedSchema as an interface model to be generated, assuming it has `oneOf` defined * * @param cs ComposedSchema object to create as interface model * @param type name to use for the generated interface model */ public void addOneOfInterfaceModel(Schema cs, String type) { if (cs.getOneOf() == null) { return; } CodegenModel cm = new CodegenModel(); cm.setDiscriminator(createDiscriminator("", cs)); for (Object o : Optional.ofNullable(cs.getOneOf()).orElse(Collections.emptyList())) { if (((Schema) o).get$ref() == null) { if (cm.discriminator != null && ((Schema) o).get$ref() == null) { // OpenAPI spec states that inline objects should not be considered when discriminator is used // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject LOGGER.warn("Ignoring inline object in oneOf definition of {}, since discriminator is used", type); } else { LOGGER.warn("Inline models are not supported in oneOf definition right now"); } continue; } cm.oneOf.add(toModelName(ModelUtils.getSimpleRef(((Schema) o).get$ref()))); } cm.name = type; cm.classname = type; cm.vendorExtensions.put("x-is-one-of-interface", true); cm.interfaceModels = new ArrayList<>(); addOneOfInterfaces.add(cm); } public void addImportsToOneOfInterface(List> imports) { } //// End of methods related to the "useOneOfInterfaces" feature protected void modifyFeatureSet(Consumer processor) { FeatureSet.Builder builder = getFeatureSet().modify(); processor.accept(builder); this.generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) .featureSet(builder.build()).build(); } /** * An map entry for cached sanitized names. */ private static class SanitizeNameOptions { public SanitizeNameOptions(String name, String removeCharRegEx, List exceptions) { this.name = name; this.removeCharRegEx = removeCharRegEx; if (exceptions != null) { this.exceptions = Collections.unmodifiableList(exceptions); } else { this.exceptions = Collections.emptyList(); } } public String getName() { return name; } public String getRemoveCharRegEx() { return removeCharRegEx; } public List getExceptions() { return exceptions; } private final String name; private final String removeCharRegEx; private final List exceptions; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SanitizeNameOptions that = (SanitizeNameOptions) o; return Objects.equals(getName(), that.getName()) && Objects.equals(getRemoveCharRegEx(), that.getRemoveCharRegEx()) && Objects.equals(getExceptions(), that.getExceptions()); } @Override public int hashCode() { return Objects.hash(getName(), getRemoveCharRegEx(), getExceptions()); } } /** * This method has been kept to keep the introduction of ModelUtils.isAnyType as a non-breaking change * this way, existing forks of our generator can continue to use this method * TODO in 6.0.0 replace this method with ModelUtils.isAnyType * Return true if the schema value can be any type, i.e. it can be * the null value, integer, number, string, object or array. * One use case is when the "type" attribute in the OAS schema is unspecified. * * Examples: * * arbitraryTypeValue: * description: This is an arbitrary type schema. * It is not a free-form object. * The value can be any type except the 'null' value. * arbitraryTypeNullableValue: * description: This is an arbitrary type schema. * It is not a free-form object. * The value can be any type, including the 'null' value. * nullable: true * * @param schema the OAS schema. * @return true if the schema value can be an arbitrary type. */ public boolean isAnyTypeSchema(Schema schema) { if (schema == null) { once(LOGGER).error("Schema cannot be null in isAnyTypeSchema check"); return false; } if (ModelUtils.isFreeFormObject(schema)) { // make sure it's not free form object return false; } if (schema.getClass().equals(Schema.class) && schema.get$ref() == null && schema.getType() == null && (schema.getProperties() == null || schema.getProperties().isEmpty()) && schema.getAdditionalProperties() == null && schema.getNot() == null && schema.getEnum() == null) { return true; // If and when type arrays are supported in a future OAS specification, // we could return true if the type array includes all possible JSON schema types. } return false; } /** * Check if the given MIME is a JSON MIME. * JSON MIME examples: * application/json * application/json; charset=UTF8 * APPLICATION/JSON * * @param mime MIME string * @return true if the input matches the JSON MIME */ public static boolean isJsonMimeType(String mime) { return mime != null && (JSON_MIME_PATTERN.matcher(mime).matches()); } public static boolean isXmlMimeType(String mime) { return mime != null && (XML_MIME_PATTERN.matcher(mime).matches()); } /** * Check if the given MIME is a JSON Vendor MIME. * JSON MIME examples: * application/vnd.mycompany+json * application/vnd.mycompany.resourceA.version1+json * * @param mime MIME string * @return true if the input matches the JSON vendor MIME */ protected static boolean isJsonVendorMimeType(String mime) { return mime != null && JSON_VENDOR_MIME_PATTERN.matcher(mime).matches(); } /** * Builds OAPI 2.0 collectionFormat value based on style and explode values * for the {@link CodegenParameter}. * * @param codegenParameter parameter * @return string for a collectionFormat. */ protected String getCollectionFormat(CodegenParameter codegenParameter) { if ("form".equals(codegenParameter.style)) { // Ref: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#style-values if (codegenParameter.isExplode) { return "multi"; } else { return "csv"; } } else if ("simple".equals(codegenParameter.style)) { return "csv"; } else if ("pipeDelimited".equals(codegenParameter.style)) { return "pipes"; } else if ("spaceDelimited".equals(codegenParameter.style)) { return "ssv"; } else { // Doesn't map to any of the collectionFormat strings return null; } } private CodegenComposedSchemas getComposedSchemas(Schema schema) { if (!(ModelUtils.isComposedSchema(schema)) && schema.getNot() == null) { return null; } Schema notSchema = schema.getNot(); CodegenProperty notProperty = null; if (notSchema != null) { notProperty = fromProperty("not_schema", notSchema, false); } List allOf = new ArrayList<>(); List oneOf = new ArrayList<>(); List anyOf = new ArrayList<>(); if (ModelUtils.isComposedSchema(schema)) { allOf = getComposedProperties(schema.getAllOf(), "all_of"); oneOf = getComposedProperties(schema.getOneOf(), "one_of"); anyOf = getComposedProperties(schema.getAnyOf(), "any_of"); } return new CodegenComposedSchemas( allOf, oneOf, anyOf, notProperty ); } private List getComposedProperties(List xOfCollection, String collectionName) { if (xOfCollection == null) { return null; } List xOf = new ArrayList<>(); Set dataTypeSet = new HashSet<>(); // to keep track of dataType int i = 0; for (Schema xOfSchema : xOfCollection) { CodegenProperty cp = fromProperty(collectionName + "_" + i, xOfSchema, false); xOf.add(cp); i += 1; if (dataTypeSet.contains(cp.dataType)) { // add "x-duplicated-data-type" to indicate if the dataType already occurs before // in other sub-schemas of allOf/anyOf/oneOf cp.vendorExtensions.putIfAbsent("x-duplicated-data-type", true); } else { dataTypeSet.add(cp.dataType); } } return xOf; } @Override public String defaultTemplatingEngine() { return "mustache"; } @Override public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.JAVA; } @Override public String generatorLanguageVersion() { return null; } @Override public List getSupportedVendorExtensions() { return new ArrayList<>(); } @Override public boolean getUseInlineModelResolver() { return true; } @Override public boolean getUseOpenAPINormalizer() { return true; } @Override public Set getOpenAPIGeneratorIgnoreList() { return openapiGeneratorIgnoreList; } /* A function to convert yaml or json ingested strings like property names And convert special characters like newline, tab, carriage return Into strings that can be rendered in the language that the generator will output to */ protected String handleSpecialCharacters(String name) { return name; } /** * Used to ensure that null or Schema is returned given an input Boolean/Schema/null * This will be used in openapi 3.1.0 spec processing to ensure that Booleans become Schemas * Because our generators only understand Schemas * Note: use getIsBooleanSchemaTrue or getIsBooleanSchemaFalse on the IJsonSchemaValidationProperties * if you need to be able to detect if the original schema's value was true or false * * @param schema the input Boolean or Schema data to convert to a Schema * @return Schema the input data converted to a Schema if possible */ protected Schema getSchemaFromBooleanOrSchema(Object schema) { if (schema == null) { return null; } else if (schema instanceof Boolean) { if (Boolean.TRUE.equals(schema)) { return trueSchema; } else if (Boolean.FALSE.equals(schema)) { return falseSchema; } // null case return null; } else if (schema instanceof Schema) { return (Schema) schema; } else { throw new IllegalArgumentException("Invalid schema type; type must be Boolean or Schema"); } } public void setAutosetConstants(boolean autosetConstants) { this.autosetConstants = autosetConstants; } /** * This method removes all constant Query, Header and Cookie Params from allParams and sets them as constantParams in the CodegenOperation. * The definition of constant is single valued required enum params. * The constantParams in the the generated code should be hardcoded to the constantValue if autosetConstants feature is enabled. * * @param operation - operation to be processed */ protected void handleConstantParams(CodegenOperation operation) { if (!autosetConstants || operation.allParams.isEmpty()) { return; } final ArrayList copy = new ArrayList<>(operation.allParams); // Remove all params from Params, Non constant params will be added back later. operation.allParams.clear(); // Finds all constant params, removes them from allParams and adds them to constant params. // Also, adds back non constant params to allParams. for (CodegenParameter p : copy) { if (p.isEnum && p.required && p._enum != null && p._enum.size() == 1) { // Add to constantParams for use in the code generation templates. operation.constantParams.add(p); if (p.isQueryParam) { operation.queryParams.removeIf(param -> param.baseName.equals(p.baseName)); } if (p.isHeaderParam) { operation.headerParams.removeIf(param -> param.baseName.equals(p.baseName)); } if (p.isCookieParam) { operation.cookieParams.removeIf(param -> param.baseName.equals(p.baseName)); } LOGGER.info("Update operation [{}]. Remove parameter [{}] because it can only have a fixed value of [{}]", operation.operationId, p.baseName, p._enum.get(0)); } else { // Add back to allParams as the param is not a constant. operation.allParams.add(p); } } operation.hasParams = !operation.allParams.isEmpty(); } }