operationEntry : toFlatten) {
+ Operation operation = operationEntry.getValue();
+ String inlineSchemaName = this.getInlineSchemaName(operationEntry.getKey(), pathname);
+ flattenRequestBody(inlineSchemaName, operation);
+ flattenParameters(inlineSchemaName, operation.getParameters(), operation.getOperationId());
+ flattenResponses(inlineSchemaName, operation);
+ }
+ }
+ }
+
+ private String getInlineSchemaName(HttpMethod httpVerb, String pathname) {
+ String name = pathname;
+ if (httpVerb.equals(HttpMethod.DELETE)) {
+ name += "_delete";
+ } else if (httpVerb.equals(HttpMethod.GET)) {
+ name += "_get";
+ } else if (httpVerb.equals(HttpMethod.HEAD)) {
+ name += "_head";
+ } else if (httpVerb.equals(HttpMethod.OPTIONS)) {
+ name += "_options";
+ } else if (httpVerb.equals(HttpMethod.PATCH)) {
+ name += "_patch";
+ } else if (httpVerb.equals(HttpMethod.POST)) {
+ name += "_post";
+ } else if (httpVerb.equals(HttpMethod.PUT)) {
+ name += "_put";
+ } else if (httpVerb.equals(HttpMethod.TRACE)) {
+ name += "_trace";
+ } else {
+ // no HTTP verb defined?
+ // throw new RuntimeException("No HTTP verb found/detected in the inline model
+ // resolver");
+ }
+ return name;
+ }
+
+ /**
+ * Return false if model can be represented by primitives e.g. string, object
+ * without properties, array or map of other model (model container), etc.
+ *
+ * Return true if a model should be generated e.g. object with properties,
+ * enum, oneOf, allOf, anyOf, etc.
+ *
+ * @param schema target schema
+ */
+ private boolean isModelNeeded(Schema schema) {
+ return isModelNeeded(schema, new HashSet<>());
+ }
+
+ /**
+ * Return false if model can be represented by primitives e.g. string, object
+ * without properties, array or map of other model (model container), etc.
+ *
+ * Return true if a model should be generated e.g. object with properties,
+ * enum, oneOf, allOf, anyOf, etc.
+ *
+ * @param schema target schema
+ * @param visitedSchemas Visited schemas
+ */
+ private boolean isModelNeeded(Schema schema, Set visitedSchemas) {
+ if (visitedSchemas.contains(schema)) { // circular reference
+ return true;
+ } else {
+ visitedSchemas.add(schema);
+ }
+
+ if (resolveInlineEnums && schema.getEnum() != null && schema.getEnum().size() > 0) {
+ return true;
+ }
+ if (schema.getType() == null || "object".equals(schema.getType())) {
+ // object or undeclared type with properties
+ if (schema.getProperties() != null && schema.getProperties().size() > 0) {
+ return true;
+ }
+ }
+ if (ModelUtils.isComposedSchema(schema)) {
+ // allOf, anyOf, oneOf
+ boolean isSingleAllOf = schema.getAllOf() != null && schema.getAllOf().size() == 1;
+ boolean isReadOnly = schema.getReadOnly() != null && schema.getReadOnly();
+ boolean isNullable = schema.getNullable() != null && schema.getNullable();
+
+ if (isSingleAllOf && (isReadOnly || isNullable)) {
+ // Check if this composed schema only contains an allOf and a readOnly or nullable.
+ ComposedSchema c = new ComposedSchema();
+ c.setAllOf(schema.getAllOf());
+ c.setReadOnly(schema.getReadOnly());
+ c.setNullable(schema.getNullable());
+ if (schema.equals(c)) {
+ return isModelNeeded((Schema) schema.getAllOf().get(0), visitedSchemas);
+ }
+ }
+
+ if (isSingleAllOf && StringUtils.isNotEmpty(((Schema) schema.getAllOf().get(0)).get$ref())) {
+ // single allOf and it's a ref
+ return isModelNeeded((Schema) schema.getAllOf().get(0), visitedSchemas);
+ }
+
+ if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) {
+ // check to ensure at least one of the allOf item is model
+ for (Object inner : schema.getAllOf()) {
+ if (isModelNeeded(ModelUtils.getReferencedSchema(openAPI, (Schema) inner), visitedSchemas)) {
+ return true;
+ }
+ }
+ // allOf items are all non-model (e.g. type: string) only
+ return false;
+ }
+
+ if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
+ return true;
+ }
+ if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Recursively gather inline models that need to be generated and
+ * replace inline schemas with $ref to schema to-be-generated.
+ *
+ * @param schema target schema
+ * @param modelPrefix model name (usually the prefix of the inline model name)
+ */
+ private void gatherInlineModels(Schema schema, String modelPrefix) {
+ if (schema.get$ref() != null) {
+ // if ref already, no inline schemas should be present but check for
+ // any to catch OpenAPI violations
+ if (isModelNeeded(schema) || "object".equals(schema.getType()) ||
+ schema.getProperties() != null || schema.getAdditionalProperties() != null ||
+ ModelUtils.isComposedSchema(schema)) {
+ LOGGER.error("Illegal schema found with $ref combined with other properties," +
+ " no properties should be defined alongside a $ref:\n " + schema.toString());
+ }
+ return;
+ }
+ // Check object models / any type models / composed models for properties,
+ // if the schema has a type defined that is not "object" it should not define
+ // any properties
+ if (schema.getType() == null || "object".equals(schema.getType())) {
+ // Check properties and recurse, each property could be its own inline model
+ Map props = schema.getProperties();
+ if (props != null) {
+ for (String propName : props.keySet()) {
+ Schema prop = props.get(propName);
+
+ if (prop == null) {
+ continue;
+ }
+
+ String schemaName = resolveModelName(prop.getTitle(), modelPrefix + "_" + propName);
+ // Recurse to create $refs for inner models
+ gatherInlineModels(prop, schemaName);
+ if (isModelNeeded(prop)) {
+ // If this schema should be split into its own model, do so
+ Schema refSchema = this.makeSchemaInComponents(schemaName, prop);
+ props.put(propName, refSchema);
+ } else if (ModelUtils.isComposedSchema(prop)) {
+ if (prop.getAllOf() != null && prop.getAllOf().size() == 1 &&
+ !(((Schema) prop.getAllOf().get(0)).getType() == null ||
+ "object".equals(((Schema) prop.getAllOf().get(0)).getType()))) {
+ // allOf with only 1 type (non-model)
+ LOGGER.info("allOf schema used by the property `{}` replaced by its only item (a type)", propName);
+ props.put(propName, (Schema) prop.getAllOf().get(0));
+ }
+ }
+ }
+ }
+ // Check additionalProperties for inline models
+ if (schema.getAdditionalProperties() != null) {
+ if (schema.getAdditionalProperties() instanceof Schema) {
+ Schema inner = (Schema) schema.getAdditionalProperties();
+ if (inner != null) {
+ String schemaName = resolveModelName(inner.getTitle(), modelPrefix + this.inlineSchemaOptions.get("MAP_ITEM_SUFFIX"));
+ // Recurse to create $refs for inner models
+ gatherInlineModels(inner, schemaName);
+ if (isModelNeeded(inner)) {
+ // If this schema should be split into its own model, do so
+ Schema refSchema = this.makeSchemaInComponents(schemaName, inner);
+ schema.setAdditionalProperties(refSchema);
+ }
+ }
+ }
+ }
+ } else if (schema.getProperties() != null) {
+ // If non-object type is specified but also properties
+ LOGGER.error("Illegal schema found with non-object type combined with properties," +
+ " no properties should be defined:\n " + schema.toString());
+ return;
+ } else if (schema.getAdditionalProperties() != null) {
+ // If non-object type is specified but also additionalProperties
+ LOGGER.error("Illegal schema found with non-object type combined with" +
+ " additionalProperties, no additionalProperties should be defined:\n " +
+ schema.toString());
+ return;
+ }
+ // Check array items
+ if (ModelUtils.isArraySchema(schema)) {
+ Schema items = ModelUtils.getSchemaItems(schema);
+ if (items == null && schema.getPrefixItems() == null) {
+ LOGGER.debug("Incorrect array schema with no items, prefixItems: {}", schema.toString());
+ return;
+ }
+
+ if (items == null) {
+ LOGGER.debug("prefixItems in array schema is not supported at the moment: {}", schema.toString());
+ return;
+ }
+ String schemaName = resolveModelName(items.getTitle(), modelPrefix + this.inlineSchemaOptions.get("ARRAY_ITEM_SUFFIX"));
+
+ // Recurse to create $refs for inner models
+ gatherInlineModels(items, schemaName);
+
+ if (isModelNeeded(items)) {
+ // If this schema should be split into its own model, do so
+ schema.setItems(this.makeSchemaInComponents(schemaName, items));
+ }
+ }
+ // Check allOf, anyOf, oneOf for inline models
+ if (ModelUtils.isComposedSchema(schema)) {
+ if (schema.getAllOf() != null) {
+ List newAllOf = new ArrayList();
+ boolean atLeastOneModel = false;
+ for (Object inner : schema.getAllOf()) {
+ if (inner == null) {
+ continue;
+ }
+ String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_allOf");
+ // Recurse to create $refs for inner models
+ gatherInlineModels((Schema) inner, schemaName);
+ if (isModelNeeded((Schema) inner)) {
+ if (Boolean.TRUE.equals(this.refactorAllOfInlineSchemas)) {
+ newAllOf.add(this.makeSchemaInComponents(schemaName, (Schema) inner)); // replace with ref
+ atLeastOneModel = true;
+ } else { // do not refactor allOf inline schemas
+ newAllOf.add((Schema) inner);
+ atLeastOneModel = true;
+ }
+ } else {
+ newAllOf.add((Schema) inner);
+ }
+ }
+ if (atLeastOneModel) {
+ schema.setAllOf(newAllOf);
+ } else {
+ // allOf is just one or more types only so do not generate the inline allOf model
+ if (schema.getAllOf().size() == 1) {
+ // handle earlier in this function when looping through properties
+ } else if (schema.getAllOf().size() > 1) {
+ LOGGER.warn("allOf schema `{}` containing multiple types (not model) is not supported at the moment.", schema.getName());
+ } else {
+ LOGGER.error("allOf schema `{}` contains no items.", schema.getName());
+ }
+ }
+ }
+ if (schema.getAnyOf() != null) {
+ List newAnyOf = new ArrayList();
+ for (Object inner : schema.getAnyOf()) {
+ if (inner == null) {
+ continue;
+ }
+ String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_anyOf");
+ // Recurse to create $refs for inner models
+ gatherInlineModels((Schema) inner, schemaName);
+ if (isModelNeeded((Schema) inner)) {
+ newAnyOf.add(this.makeSchemaInComponents(schemaName, (Schema) inner)); // replace with ref
+ } else {
+ newAnyOf.add((Schema) inner);
+ }
+ }
+ schema.setAnyOf(newAnyOf);
+ }
+ if (schema.getOneOf() != null) {
+ List newOneOf = new ArrayList();
+ for (Object inner : schema.getOneOf()) {
+ if (inner == null) {
+ continue;
+ }
+ String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_oneOf");
+ // Recurse to create $refs for inner models
+ gatherInlineModels((Schema) inner, schemaName);
+ if (isModelNeeded((Schema) inner)) {
+ newOneOf.add(this.makeSchemaInComponents(schemaName, (Schema) inner)); // replace with ref
+ } else {
+ newOneOf.add((Schema) inner);
+ }
+ }
+ schema.setOneOf(newOneOf);
+ }
+ }
+ // Check not schema
+ if (schema.getNot() != null) {
+ Schema not = schema.getNot();
+ if (not != null) {
+ String schemaName = resolveModelName(schema.getTitle(), modelPrefix + "_not");
+ // Recurse to create $refs for inner models
+ gatherInlineModels(not, schemaName);
+ if (isModelNeeded(not)) {
+ Schema refSchema = this.makeSchemaInComponents(schemaName, not);
+ schema.setNot(refSchema);
+ }
+ }
+ }
+ }
+
+ /**
+ * Flatten inline models in content
+ *
+ * @param content target content
+ * @param name backup name if no title is found
+ */
+ private void flattenContent(Content content, String name) {
+ if (content == null || content.isEmpty()) {
+ return;
+ }
+
+ for (String contentType : content.keySet()) {
+ MediaType mediaType = content.get(contentType);
+ if (mediaType == null) {
+ continue;
+ }
+ Schema schema = mediaType.getSchema();
+ if (schema == null) {
+ continue;
+ }
+ String schemaName = resolveModelName(schema.getTitle(), name); // name example: testPost_request
+ // Recursively gather/make inline models within this schema if any
+ gatherInlineModels(schema, schemaName);
+ if (isModelNeeded(schema)) {
+ // If this schema should be split into its own model, do so
+ //Schema refSchema = this.makeSchema(schemaName, schema);
+ mediaType.setSchema(this.makeSchemaInComponents(schemaName, schema));
+ }
+ }
+ }
+
+ /**
+ * Flatten inline models in RequestBody
+ *
+ * @param modelName inline model name prefix
+ * @param operation target operation
+ */
+ private void flattenRequestBody(String modelName, Operation operation) {
+ RequestBody requestBody = operation.getRequestBody();
+ if (requestBody == null) {
+ return;
+ }
+
+ // unalias $ref
+ if (requestBody.get$ref() != null) {
+ String ref = ModelUtils.getSimpleRef(requestBody.get$ref());
+ requestBody = openAPI.getComponents().getRequestBodies().get(ref);
+
+ if (requestBody == null) {
+ return;
+ }
+ }
+
+ flattenContent(requestBody.getContent(),
+ (operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_request");
+ }
+
+ /**
+ * Flatten inline models in parameters
+ *
+ * @param modelName model name
+ * @param parameters list of parameters
+ * @param operationId operation Id (optional)
+ */
+ private void flattenParameters(String modelName, List parameters, String operationId) {
+ //List parameters = operation.getParameters();
+ if (parameters == null) {
+ return;
+ }
+
+ for (Parameter parameter : parameters) {
+ if (StringUtils.isNotEmpty(parameter.get$ref())) {
+ parameter = ModelUtils.getReferencedParameter(openAPI, parameter);
+ }
+
+ if (parameter.getSchema() == null) {
+ continue;
+ }
+
+ Schema parameterSchema = parameter.getSchema();
+
+ if (parameterSchema == null) {
+ continue;
+ }
+ String schemaName = resolveModelName(parameterSchema.getTitle(),
+ (operationId == null ? modelName : operationId) + "_" + parameter.getName() + "_parameter");
+ // Recursively gather/make inline models within this schema if any
+ gatherInlineModels(parameterSchema, schemaName);
+ if (isModelNeeded(parameterSchema)) {
+ // If this schema should be split into its own model, do so
+ parameter.setSchema(this.makeSchemaInComponents(schemaName, parameterSchema));
+ }
+ }
+ }
+
+ /**
+ * Flatten inline models in ApiResponses
+ *
+ * @param modelName model name prefix
+ * @param operation target operation
+ */
+ private void flattenResponses(String modelName, Operation operation) {
+ ApiResponses responses = operation.getResponses();
+ if (responses == null) {
+ return;
+ }
+
+ for (Map.Entry responsesEntry : responses.entrySet()) {
+ String key = responsesEntry.getKey();
+ ApiResponse response = responsesEntry.getValue();
+
+ flattenContent(response.getContent(),
+ (operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + key + "_response");
+ }
+ }
+
+ /**
+ * Flatten inline models in the responses section in the components.
+ */
+ private void flattenComponentResponses() {
+ Map apiResponses = openAPI.getComponents().getResponses();
+ if (apiResponses == null) {
+ return;
+ }
+
+ for (Map.Entry entry : apiResponses.entrySet()) {
+ flattenContent(entry.getValue().getContent(), null);
+ }
+ }
+
+ /**
+ * Flattens properties of inline object schemas that belong to a composed schema into a
+ * single flat list of properties. This is useful to generate a single or multiple
+ * inheritance model.
+ *
+ * In the example below, codegen may generate a 'Dog' class that extends from the
+ * generated 'Animal' class. 'Dog' has additional properties 'name', 'age' and 'breed' that
+ * are flattened as a single list of properties.
+ *
+ * Dog:
+ * allOf:
+ * - $ref: '#/components/schemas/Animal'
+ * - type: object
+ * properties:
+ * name:
+ * type: string
+ * age:
+ * type: string
+ * - type: object
+ * properties:
+ * breed:
+ * type: string
+ *
+ * @param key a unique name for the composed schema.
+ * @param children the list of nested schemas within a composed schema (allOf, anyOf, oneOf).
+ * @param skipAllOfInlineSchemas true if allOf inline schemas need to be skipped.
+ */
+ private void flattenComposedChildren(String key, List children, boolean skipAllOfInlineSchemas) {
+ if (children == null || children.isEmpty()) {
+ return;
+ }
+ ListIterator listIterator = children.listIterator();
+ while (listIterator.hasNext()) {
+ Schema component = listIterator.next();
+ if ((component != null) &&
+ (component.get$ref() == null) &&
+ ((component.getProperties() != null && !component.getProperties().isEmpty()) ||
+ (component.getEnum() != null && !component.getEnum().isEmpty()))) {
+ // If a `title` attribute is defined in the inline schema, codegen uses it to name the
+ // inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
+ // We know that this is not the best way to name the model.
+ //
+ // Such naming strategy may result in issues. If the value of the 'title' attribute
+ // happens to match a schema defined elsewhere in the specification, 'innerModelName'
+ // will be the same as that other schema.
+ //
+ // To have complete control of the model naming, one can define the model separately
+ // instead of inline.
+ String innerModelName = resolveModelName(component.getTitle(), key);
+ Schema innerModel = modelFromProperty(openAPI, component, innerModelName);
+ // Recurse to create $refs for inner models
+ gatherInlineModels(innerModel, innerModelName);
+ if (!skipAllOfInlineSchemas) {
+ String existing = matchGenerated(innerModel);
+ if (existing == null) {
+ innerModelName = addSchemas(innerModelName, innerModel);
+ Schema schema = new Schema().$ref(innerModelName);
+ schema.setRequired(component.getRequired());
+ listIterator.set(schema);
+ } else {
+ Schema schema = new Schema().$ref(existing);
+ schema.setRequired(component.getRequired());
+ listIterator.set(schema);
+ }
+ } else {
+ LOGGER.debug("Inline allOf schema {} not refactored into a separate model using $ref.", innerModelName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Flatten inline models in components
+ */
+ private void flattenComponents() {
+ Map models = openAPI.getComponents().getSchemas();
+ if (models == null) {
+ return;
+ }
+
+ List modelNames = new ArrayList(models.keySet());
+ for (String modelName : modelNames) {
+ Schema model = models.get(modelName);
+ if (model == null) {
+ continue;
+ }
+ if (ModelUtils.isAnyOf(model)) { // contains anyOf only
+ gatherInlineModels(model, modelName);
+ } else if (ModelUtils.isOneOf(model)) { // contains oneOf only
+ gatherInlineModels(model, modelName);
+ } else if (ModelUtils.isComposedSchema(model)) {
+ // inline child schemas
+ flattenComposedChildren(modelName + "_allOf", model.getAllOf(), !Boolean.TRUE.equals(this.refactorAllOfInlineSchemas));
+ flattenComposedChildren(modelName + "_anyOf", model.getAnyOf(), false);
+ flattenComposedChildren(modelName + "_oneOf", model.getOneOf(), false);
+ } else {
+ gatherInlineModels(model, modelName);
+ }
+ }
+ }
+
+ /**
+ * This function fix models that are string (mostly enum). Before this fix, the
+ * example would look something like that in the doc: "\"example from def\""
+ *
+ * @param m Schema implementation
+ */
+ private void fixStringModel(Schema m) {
+ if (schemaIsOfType(m, "string") && schemaContainsExample(m)) {
+ String example = m.getExample().toString();
+ if (example.startsWith("\"") && example.endsWith("\"")) {
+ m.setExample(example.substring(1, example.length() - 1));
+ }
+ }
+ }
+
+ private boolean schemaIsOfType(Schema m, String type) {
+ return m.getType() != null && m.getType().equals(type);
+ }
+
+ private boolean schemaContainsExample(Schema m) {
+ return m.getExample() != null && m.getExample() != "";
+ }
+
+ /**
+ * Generates a unique model name. Non-alphanumeric characters will be replaced
+ * with underscores
+ *
+ * e.g. io.schema.User_name => io_schema_User_name
+ *
+ * @param title String title field in the schema if present
+ * @param modelName String model name
+ * @return if provided the sanitized {@code title}, else the sanitized {@code key}
+ */
+ private String resolveModelName(String title, String modelName) {
+ if (title == null || "".equals(sanitizeName(title).replace("_", ""))) {
+ if (modelName == null) {
+ return uniqueName("inline_object");
+ }
+ return uniqueName(sanitizeName(modelName));
+ } else {
+ return uniqueName(sanitizeName(title));
+ }
+ }
+
+ private String matchGenerated(Schema model) {
+ if (skipSchemaReuse) { // skip reusing schema
+ return null;
+ }
+
+ try {
+ String json = structureMapper.writeValueAsString(model);
+ if (generatedSignature.containsKey(json)) {
+ return generatedSignature.get(json);
+ }
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ private void addGenerated(String name, Schema model) {
+ try {
+ String json = structureMapper.writeValueAsString(model);
+ generatedSignature.put(json, name);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Sanitizes the input so that it's valid name for a class or interface
+ *
+ * e.g. 12.schema.User name => _2_schema_User_name
+ *
+ * @param name name to be processed to make sure it's sanitized
+ */
+ private String sanitizeName(final String name) {
+ return name
+ .replaceAll("^[0-9]", "_$0") // e.g. 12object => _12object
+ .replaceAll("[^A-Za-z0-9]", "_"); // e.g. io.schema.User name => io_schema_User_name
+ }
+
+ /**
+ * Generate a unique name for the input
+ *
+ * @param name name to be processed to make sure it's unique
+ */
+ private String uniqueName(final String name) {
+ if (openAPI.getComponents().getSchemas() == null) { // no schema has been created
+ return name;
+ }
+
+ String uniqueName = name;
+ int count = 0;
+ while (true) {
+ if (!openAPI.getComponents().getSchemas().containsKey(uniqueName) && !uniqueNames.contains(uniqueName)) {
+ return uniqueName;
+ }
+ uniqueName = name + "_" + ++count;
+ }
+ }
+
+ private void flattenProperties(OpenAPI openAPI, Map properties, String path) {
+ if (properties == null) {
+ return;
+ }
+ Map propsToUpdate = new HashMap();
+ Map modelsToAdd = new HashMap();
+ for (Map.Entry propertiesEntry : properties.entrySet()) {
+ String key = propertiesEntry.getKey();
+ Schema property = propertiesEntry.getValue();
+ if (ModelUtils.isObjectSchema(property)) {
+ Schema op = property;
+ String modelName = resolveModelName(op.getTitle(), path + "_" + key);
+ Schema model = modelFromProperty(openAPI, op, modelName);
+ String existing = matchGenerated(model);
+ if (existing != null) {
+ Schema schema = new Schema().$ref(existing);
+ schema.setRequired(op.getRequired());
+ propsToUpdate.put(key, schema);
+ } else {
+ modelName = addSchemas(modelName, model);
+ Schema schema = new Schema().$ref(modelName);
+ schema.setRequired(op.getRequired());
+ propsToUpdate.put(key, schema);
+ modelsToAdd.put(modelName, model);
+ }
+ } else if (ModelUtils.isArraySchema(property)) {
+ Schema inner = ModelUtils.getSchemaItems(property);
+ if (ModelUtils.isObjectSchema(inner)) {
+ Schema op = inner;
+ if (op.getProperties() != null && op.getProperties().size() > 0) {
+ flattenProperties(openAPI, op.getProperties(), path);
+ String modelName = resolveModelName(op.getTitle(), path + "_" + key);
+ Schema innerModel = modelFromProperty(openAPI, op, modelName);
+ String existing = matchGenerated(innerModel);
+ if (existing != null) {
+ Schema schema = new Schema().$ref(existing);
+ schema.setRequired(op.getRequired());
+ property.setItems(schema);
+ } else {
+ modelName = addSchemas(modelName, innerModel);
+ Schema schema = new Schema().$ref(modelName);
+ schema.setRequired(op.getRequired());
+ property.setItems(schema);
+ }
+ }
+ } else if (ModelUtils.isComposedSchema(inner)) {
+ String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
+ gatherInlineModels(inner, innerModelName);
+ innerModelName = addSchemas(innerModelName, inner);
+ Schema schema = new Schema().$ref(innerModelName);
+ schema.setRequired(inner.getRequired());
+ property.setItems(schema);
+ } else {
+ LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
+ }
+ } else if (ModelUtils.isMapSchema(property)) {
+ Schema inner = ModelUtils.getAdditionalProperties(property);
+ if (ModelUtils.isObjectSchema(inner)) {
+ Schema op = inner;
+ if (op.getProperties() != null && op.getProperties().size() > 0) {
+ flattenProperties(openAPI, op.getProperties(), path);
+ String modelName = resolveModelName(op.getTitle(), path + "_" + key);
+ Schema innerModel = modelFromProperty(openAPI, op, modelName);
+ String existing = matchGenerated(innerModel);
+ if (existing != null) {
+ Schema schema = new Schema().$ref(existing);
+ schema.setRequired(op.getRequired());
+ property.setAdditionalProperties(schema);
+ } else {
+ modelName = addSchemas(modelName, innerModel);
+ Schema schema = new Schema().$ref(modelName);
+ schema.setRequired(op.getRequired());
+ property.setAdditionalProperties(schema);
+ }
+ }
+ } else if (ModelUtils.isComposedSchema(inner)) {
+ String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
+ gatherInlineModels(inner, innerModelName);
+ innerModelName = addSchemas(innerModelName, inner);
+ Schema schema = new Schema().$ref(innerModelName);
+ schema.setRequired(inner.getRequired());
+ property.setAdditionalProperties(schema);
+ } else {
+ LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
+ }
+ } else if (ModelUtils.isComposedSchema(property)) { // oneOf, anyOf, allOf etc
+ if (property.getAllOf() != null && property.getAllOf().size() == 1 // allOf with a single item
+ && (property.getOneOf() == null || property.getOneOf().isEmpty()) // not oneOf
+ && (property.getAnyOf() == null || property.getAnyOf().isEmpty()) // not anyOf
+ && (property.getProperties() == null || property.getProperties().isEmpty())) { // no property
+ // don't do anything if it's allOf with a single item
+ LOGGER.debug("allOf with a single item (which can be handled by default codegen) skipped by inline model resolver: {}", property);
+ } else {
+ String propertyModelName = resolveModelName(property.getTitle(), path + "_" + key);
+ gatherInlineModels(property, propertyModelName);
+ propertyModelName = addSchemas(propertyModelName, property);
+ Schema schema = new Schema().$ref(propertyModelName);
+ schema.setRequired(property.getRequired());
+ propsToUpdate.put(key, schema);
+ }
+ } else {
+ LOGGER.debug("Schema not yet handled in model resolver: {}", property);
+ }
+ }
+ if (propsToUpdate.size() > 0) {
+ for (String key : propsToUpdate.keySet()) {
+ properties.put(key, propsToUpdate.get(key));
+ }
+ }
+ for (String key : modelsToAdd.keySet()) {
+ openAPI.getComponents().addSchemas(key, modelsToAdd.get(key));
+ this.addedModels.put(key, modelsToAdd.get(key));
+ }
+ }
+
+ private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) {
+ String description = object.getDescription();
+ String example = null;
+ Object obj = object.getExample();
+ if (obj != null) {
+ example = obj.toString();
+ }
+ XML xml = object.getXml();
+ Map properties = object.getProperties();
+
+ // NOTE:
+ // No need to null check setters below. All defaults in the new'd Schema are null, so setting to null would just be a noop.
+ Schema model = new Schema();
+ model.setType(object.getType());
+
+ // Even though the `format` keyword typically applies to primitive types only,
+ // the JSON schema specification states `format` can be used for any model type instance
+ // including object types.
+ model.setFormat(object.getFormat());
+
+ if (object.getExample() != null) {
+ model.setExample(example);
+ }
+ model.setDescription(description);
+ model.setName(object.getName());
+ model.setXml(xml);
+ model.setRequired(object.getRequired());
+ model.setNullable(object.getNullable());
+ model.setEnum(object.getEnum());
+ model.setType(object.getType());
+ model.setDiscriminator(object.getDiscriminator());
+ model.setWriteOnly(object.getWriteOnly());
+ model.setUniqueItems(object.getUniqueItems());
+ model.setTitle(object.getTitle());
+ model.setReadOnly(object.getReadOnly());
+ model.setPattern(object.getPattern());
+ model.setNot(object.getNot());
+ model.setMinProperties(object.getMinProperties());
+ model.setMinLength(object.getMinLength());
+ model.setMinItems(object.getMinItems());
+ model.setMinimum(object.getMinimum());
+ model.setMaxProperties(object.getMaxProperties());
+ model.setMaxLength(object.getMaxLength());
+ model.setMaxItems(object.getMaxItems());
+ model.setMaximum(object.getMaximum());
+ model.setExternalDocs(object.getExternalDocs());
+ model.setExtensions(object.getExtensions());
+ model.setExclusiveMinimum(object.getExclusiveMinimum());
+ model.setExclusiveMaximum(object.getExclusiveMaximum());
+ // no need to set it again as it's set earlier
+ //model.setExample(object.getExample());
+ model.setDeprecated(object.getDeprecated());
+
+ if (properties != null) {
+ flattenProperties(openAPI, properties, path);
+ model.setProperties(properties);
+ }
+ return model;
+ }
+
+ /**
+ * Move schema to components (if new) and return $ref to schema or
+ * existing schema.
+ *
+ * @param name new schema name
+ * @param schema schema to move to components or find existing ref
+ * @return {@link Schema} $ref schema to new or existing schema
+ */
+ private Schema makeSchemaInComponents(String name, Schema schema) {
+ String existing = matchGenerated(schema);
+ Schema refSchema;
+ if (existing != null) {
+ refSchema = new Schema().$ref(existing);
+ } else {
+ if (resolveInlineEnums && schema.getEnum() != null && schema.getEnum().size() > 0) {
+ LOGGER.warn("Model " + name + " promoted to its own schema due to resolveInlineEnums=true");
+ }
+ name = addSchemas(name, schema);
+ refSchema = new Schema().$ref(name);
+ }
+ this.copyVendorExtensions(schema, refSchema);
+
+ return refSchema;
+ }
+
+ /**
+ * Make a Schema
+ *
+ * @param ref new property name
+ * @param property Schema
+ * @return {@link Schema} A constructed OpenAPI property
+ */
+ private Schema makeSchema(String ref, Schema property) {
+ Schema newProperty = new Schema().$ref(ref);
+ this.copyVendorExtensions(property, newProperty);
+ return newProperty;
+ }
+
+ /**
+ * Copy vendor extensions from Model to another Model
+ *
+ * @param source source property
+ * @param target target property
+ */
+ private void copyVendorExtensions(Schema source, Schema target) {
+ Map vendorExtensions = source.getExtensions();
+ if (vendorExtensions == null) {
+ return;
+ }
+ for (String extName : vendorExtensions.keySet()) {
+ target.addExtension(extName, vendorExtensions.get(extName));
+ }
+ }
+
+ /**
+ * Add the schemas to the components
+ *
+ * @param name name of the inline schema
+ * @param schema inline schema
+ * @return the actual model name (based on inlineSchemaNameMapping if provided)
+ */
+ private String addSchemas(String name, Schema schema) {
+ //check inlineSchemaNameMapping
+ if (inlineSchemaNameMapping.containsKey(name)) {
+ name = inlineSchemaNameMapping.get(name);
+ }
+
+ addGenerated(name, schema);
+ openAPI.getComponents().addSchemas(name, schema);
+ if (!name.equals(schema.getTitle()) && !inlineSchemaNameMappingValues.contains(name)) {
+ LOGGER.info("Inline schema created as {}. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings {}=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings {}=NewModel,ModelA=NewModelA in CLI).", name, name, name);
+ }
+
+ uniqueNames.add(name);
+
+ return name;
+ }
+}
diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/GlobalSettings.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/GlobalSettings.java
new file mode 100644
index 00000000000..ab14bf30123
--- /dev/null
+++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/GlobalSettings.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.swagger.codegen.v3.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Properties;
+
+/**
+ * GlobalSettings encapsulates SystemProperties, since the codegen mechanism heavily relies on a stable,
+ * non-changing System Property Basis. Using plain System.(get|set|clear)Property raises Race-Conditions in combination
+ * with Code, that uses System.setProperties (e.g. maven-surefire-plugin).
+ *
+ * This provides a set of properties specific to the executing thread, such that the generator may not modify system properties
+ * consumed by other threads.
+ *
+ * @author gndrm
+ * @since 2018
+ */
+public class GlobalSettings {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GlobalSettings.class);
+
+ private static ThreadLocal properties = new InheritableThreadLocal() {
+ @Override
+ protected Properties initialValue() {
+ // avoid using System.getProperties().clone() which is broken in Gradle - see https://github.com/gradle/gradle/issues/17344
+ Properties copy = new Properties();
+ System.getProperties()
+ .forEach((k,v) -> copy.put(String.valueOf(k), String.valueOf(v)));
+ return copy;
+ }
+ };
+
+ public static String getProperty(String key, String defaultValue) {
+ return properties.get().getProperty(key, defaultValue);
+ }
+
+ public static String getProperty(String key) {
+ return properties.get().getProperty(key);
+ }
+
+ public static void setProperty(String key, String value) {
+ properties.get().setProperty(key, value);
+ }
+
+ public static void clearProperty(String key) {
+ properties.get().remove(key);
+ }
+
+ public static void reset() {
+ properties.remove();
+ }
+
+ public static void log() {
+ if(LOGGER.isDebugEnabled()) {
+ StringWriter stringWriter = new StringWriter();
+ properties.get().list(new PrintWriter(stringWriter));
+ LOGGER.debug("GlobalSettings: {}", stringWriter);
+ }
+ }
+}
diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/ModelUtils.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/ModelUtils.java
index 1908cde0ce9..6719c886d8c 100644
--- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/ModelUtils.java
+++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/ModelUtils.java
@@ -1,21 +1,33 @@
package io.swagger.codegen.v3.utils;
+import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.codegen.v3.CodegenConstants;
import io.swagger.codegen.v3.CodegenModel;
import io.swagger.codegen.v3.CodegenProperty;
+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.headers.Header;
+import io.swagger.v3.oas.models.media.*;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.parameters.RequestBody;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.parser.ObjectMapperFactory;
+import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import static io.swagger.codegen.v3.utils.OnceLogger.once;
public class ModelUtils {
/**
* Searches for the model by name in the map of models and returns it
@@ -242,4 +254,1557 @@ private static String toEnumValue(String value, String datatype) {
return String.format("\"%s\"", value);
}
}
+
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class);
+
+ private static final String URI_FORMAT = "uri";
+
+ private static final String generateAliasAsModelKey = "generateAliasAsModel";
+
+ // A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI
+ private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present";
+
+ private static final String freeFormExplicit = "x-is-free-form";
+
+ private static final ObjectMapper JSON_MAPPER;
+ private static final ObjectMapper YAML_MAPPER;
+
+ static {
+ JSON_MAPPER = ObjectMapperFactory.createJson();
+ YAML_MAPPER = ObjectMapperFactory.createYaml();
+ }
+
+ public static boolean isDisallowAdditionalPropertiesIfNotPresent() {
+ return Boolean.parseBoolean(GlobalSettings.getProperty(disallowAdditionalPropertiesIfNotPresent, "true"));
+ }
+
+ public static boolean isGenerateAliasAsModel() {
+ return Boolean.parseBoolean(GlobalSettings.getProperty(generateAliasAsModelKey, "false"));
+ }
+
+
+ public static boolean isGenerateAliasAsModel(Schema schema) {
+ return isGenerateAliasAsModel() || (schema.getExtensions() != null && schema.getExtensions().getOrDefault("x-generate-alias-as-model", false).equals(true));
+ }
+
+ /**
+ * Return the list of all schemas in the 'components/schemas' section used in the openAPI specification
+ *
+ * @param openAPI specification
+ * @return schemas a list of used schemas
+ */
+ public static List getAllUsedSchemas(OpenAPI openAPI) {
+ Map> childrenMap = getChildrenMap(openAPI);
+ List allUsedSchemas = new ArrayList();
+ visitOpenAPI(openAPI, (s, t) -> {
+ if (s.get$ref() != null) {
+ String ref = getSimpleRef(s.get$ref());
+ if (!allUsedSchemas.contains(ref)) {
+ allUsedSchemas.add(ref);
+ }
+ if (childrenMap.containsKey(ref)) {
+ for (String child : childrenMap.get(ref)) {
+ if (!allUsedSchemas.contains(child)) {
+ allUsedSchemas.add(child);
+ }
+ }
+ }
+ }
+ });
+ return allUsedSchemas;
+ }
+
+ /**
+ * Return the list of unused schemas in the 'components/schemas' section of an openAPI specification
+ *
+ * @param openAPI specification
+ * @return schemas a list of unused schemas
+ */
+ public static List getUnusedSchemas(OpenAPI openAPI) {
+ final Map> childrenMap;
+ Map> tmpChildrenMap;
+ try {
+ tmpChildrenMap = getChildrenMap(openAPI);
+ } catch (NullPointerException npe) {
+ // in rare cases, such as a spec document with only one top-level oneOf schema and multiple referenced schemas,
+ // the stream used in getChildrenMap will raise an NPE. Rather than modify getChildrenMap which is used by getAllUsedSchemas,
+ // we'll catch here as a workaround for this edge case.
+ tmpChildrenMap = new HashMap<>();
+ }
+
+ childrenMap = tmpChildrenMap;
+ List unusedSchemas = new ArrayList();
+
+ if (openAPI != null) {
+ Map schemas = getSchemas(openAPI);
+ unusedSchemas.addAll(schemas.keySet());
+
+ visitOpenAPI(openAPI, (s, t) -> {
+ if (s.get$ref() != null) {
+ String ref = getSimpleRef(s.get$ref());
+ unusedSchemas.remove(ref);
+ if (childrenMap.containsKey(ref)) {
+ unusedSchemas.removeAll(childrenMap.get(ref));
+ }
+ }
+ });
+ }
+ return unusedSchemas;
+ }
+
+ /**
+ * Return the list of schemas in the 'components/schemas' used only in a 'application/x-www-form-urlencoded' or 'multipart/form-data' mime time
+ *
+ * @param openAPI specification
+ * @return schemas a list of schemas
+ */
+ public static List getSchemasUsedOnlyInFormParam(OpenAPI openAPI) {
+ List schemasUsedInFormParam = new ArrayList();
+ List schemasUsedInOtherCases = new ArrayList();
+
+ visitOpenAPI(openAPI, (s, t) -> {
+ if (s != null && s.get$ref() != null) {
+ String ref = getSimpleRef(s.get$ref());
+ if ("application/x-www-form-urlencoded".equalsIgnoreCase(t) ||
+ "multipart/form-data".equalsIgnoreCase(t)) {
+ schemasUsedInFormParam.add(ref);
+ } else {
+ schemasUsedInOtherCases.add(ref);
+ }
+ }
+ });
+ return schemasUsedInFormParam.stream().filter(n -> !schemasUsedInOtherCases.contains(n)).collect(Collectors.toList());
+ }
+
+ /**
+ * Private method used by several methods ({@link #getAllUsedSchemas(OpenAPI)},
+ * {@link #getUnusedSchemas(OpenAPI)},
+ * {@link #getSchemasUsedOnlyInFormParam(OpenAPI)}, ...) to traverse all paths of an
+ * OpenAPI instance and call the visitor functional interface when a schema is found.
+ *
+ * @param openAPI specification
+ * @param visitor functional interface (can be defined as a lambda) called each time a schema is found.
+ */
+ private static void visitOpenAPI(OpenAPI openAPI, OpenAPISchemaVisitor visitor) {
+ Map paths = openAPI.getPaths();
+ List visitedSchemas = new ArrayList<>();
+
+ if (paths != null) {
+ for (PathItem path : paths.values()) {
+ visitPathItem(path, openAPI, visitor, visitedSchemas);
+ }
+ }
+ }
+
+ private static void visitPathItem(PathItem pathItem, OpenAPI openAPI, OpenAPISchemaVisitor visitor, List visitedSchemas) {
+ List allOperations = pathItem.readOperations();
+ if (allOperations != null) {
+ for (Operation operation : allOperations) {
+ //Params:
+ visitParameters(openAPI, operation.getParameters(), visitor, visitedSchemas);
+
+ //RequestBody:
+ RequestBody requestBody = getReferencedRequestBody(openAPI, operation.getRequestBody());
+ if (requestBody != null) {
+ visitContent(openAPI, requestBody.getContent(), visitor, visitedSchemas);
+ }
+
+ //Responses:
+ if (operation.getResponses() != null) {
+ for (ApiResponse r : operation.getResponses().values()) {
+ ApiResponse apiResponse = getReferencedApiResponse(openAPI, r);
+ if (apiResponse != null) {
+ visitContent(openAPI, apiResponse.getContent(), visitor, visitedSchemas);
+ if (apiResponse.getHeaders() != null) {
+ for (Map.Entry e : apiResponse.getHeaders().entrySet()) {
+ Header header = getReferencedHeader(openAPI, e.getValue());
+ if (header.getSchema() != null) {
+ visitSchema(openAPI, header.getSchema(), e.getKey(), visitedSchemas, visitor);
+ }
+ visitContent(openAPI, header.getContent(), visitor, visitedSchemas);
+ }
+ }
+ }
+ }
+ }
+
+ //Callbacks:
+ if (operation.getCallbacks() != null) {
+ for (Callback c : operation.getCallbacks().values()) {
+ Callback callback = getReferencedCallback(openAPI, c);
+ if (callback != null) {
+ for (PathItem p : callback.values()) {
+ visitPathItem(p, openAPI, visitor, visitedSchemas);
+ }
+ }
+ }
+ }
+ }
+ }
+ //Params:
+ visitParameters(openAPI, pathItem.getParameters(), visitor, visitedSchemas);
+ }
+
+ private static void visitParameters(OpenAPI openAPI, List parameters, OpenAPISchemaVisitor visitor,
+ List visitedSchemas) {
+ if (parameters != null) {
+ for (Parameter p : parameters) {
+ Parameter parameter = getReferencedParameter(openAPI, p);
+ if (parameter != null) {
+ if (parameter.getSchema() != null) {
+ visitSchema(openAPI, parameter.getSchema(), null, visitedSchemas, visitor);
+ }
+ visitContent(openAPI, parameter.getContent(), visitor, visitedSchemas);
+ } else {
+ once(LOGGER).warn("Unreferenced parameter(s) found.");
+ }
+ }
+ }
+ }
+
+ private static void visitContent(OpenAPI openAPI, Content content, OpenAPISchemaVisitor visitor, List visitedSchemas) {
+ if (content != null) {
+ for (Map.Entry e : content.entrySet()) {
+ if (e.getValue().getSchema() != null) {
+ visitSchema(openAPI, e.getValue().getSchema(), e.getKey(), visitedSchemas, visitor);
+ }
+ }
+ }
+ }
+
+ /**
+ * Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document.
+ *
+ * To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited,
+ * it is added to visitedSchemas.
+ *
+ * @param openAPI the OpenAPI document that contains schema objects.
+ * @param schema the root schema object to be visited.
+ * @param mimeType the mime type. TODO: does not seem to be used in a meaningful way.
+ * @param visitedSchemas the list of referenced schemas that have been visited.
+ * @param visitor the visitor function which is invoked for every visited schema.
+ */
+ private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List visitedSchemas, OpenAPISchemaVisitor visitor) {
+ if (schema == null) {
+ return;
+ }
+
+ visitor.visit(schema, mimeType);
+ if (schema.get$ref() != null) {
+ String ref = getSimpleRef(schema.get$ref());
+ if (!visitedSchemas.contains(ref)) {
+ visitedSchemas.add(ref);
+ Schema referencedSchema = getSchemas(openAPI).get(ref);
+ if (referencedSchema != null) {
+ visitSchema(openAPI, referencedSchema, mimeType, visitedSchemas, visitor);
+ }
+ }
+ }
+ if (isComposedSchema(schema)) {
+ List oneOf = schema.getOneOf();
+ if (oneOf != null) {
+ for (Schema s : oneOf) {
+ visitSchema(openAPI, s, mimeType, visitedSchemas, visitor);
+ }
+ }
+ List allOf = schema.getAllOf();
+ if (allOf != null) {
+ for (Schema s : allOf) {
+ visitSchema(openAPI, s, mimeType, visitedSchemas, visitor);
+ }
+ }
+ List anyOf = schema.getAnyOf();
+ if (anyOf != null) {
+ for (Schema s : anyOf) {
+ visitSchema(openAPI, s, mimeType, visitedSchemas, visitor);
+ }
+ }
+ } else if (ModelUtils.isArraySchema(schema)) {
+ Schema itemsSchema = ModelUtils.getSchemaItems(schema);
+ if (itemsSchema != null) {
+ visitSchema(openAPI, itemsSchema, mimeType, visitedSchemas, visitor);
+ }
+ } else if (isMapSchema(schema)) {
+ Object additionalProperties = schema.getAdditionalProperties();
+ if (additionalProperties instanceof Schema) {
+ visitSchema(openAPI, (Schema) additionalProperties, mimeType, visitedSchemas, visitor);
+ }
+ }
+ if (schema.getNot() != null) {
+ visitSchema(openAPI, schema.getNot(), mimeType, visitedSchemas, visitor);
+ }
+ Map properties = schema.getProperties();
+ if (properties != null) {
+ for (Schema property : properties.values()) {
+ visitSchema(openAPI, property, null, visitedSchemas, visitor);
+ }
+ }
+ }
+
+ public static String getSimpleRef(String ref) {
+ if (ref == null) {
+ once(LOGGER).warn("Failed to get the schema name: null");
+ //throw new RuntimeException("Failed to get the schema: null");
+ return null;
+ } else if (ref.startsWith("#/components/")) {
+ ref = ref.substring(ref.lastIndexOf("/") + 1);
+ } else if (ref.startsWith("#/definitions/")) {
+ ref = ref.substring(ref.lastIndexOf("/") + 1);
+ } else {
+ once(LOGGER).warn("Failed to get the schema name: {}", ref);
+ //throw new RuntimeException("Failed to get the schema: " + ref);
+ return null;
+ }
+
+ try {
+ ref = URLDecoder.decode(ref,String.valueOf(StandardCharsets.UTF_8) );
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+
+ // see https://tools.ietf.org/html/rfc6901#section-3
+ // Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in
+ // JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded
+ // as '~1' when these characters appear in a reference token.
+ // This reverses that encoding.
+ ref = ref.replace("~1", "/").replace("~0", "~");
+
+ return ref;
+ }
+
+ /**
+ * Return true if the specified schema is type object
+ * We can't use isObjectSchema because it requires properties to exist which is not required
+ * We can't use isMap because it is true for AnyType use cases
+ *
+ * @param schema the OAS schema
+ * @return true if the specified schema is an Object schema.
+ */
+ public static boolean isTypeObjectSchema(Schema schema) {
+ return SchemaTypeUtil.OBJECT_TYPE.equals(getType(schema));
+ }
+
+ /**
+ * Return true if the specified schema is an object with a fixed number of properties.
+ *
+ * A ObjectSchema differs from a MapSchema in the following way:
+ * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties.
+ * - A MapSchema is an object that can be extended with an arbitrary set of properties.
+ * The payload may include dynamic properties.
+ *
+ * For example, an OpenAPI schema is considered an ObjectSchema in the following scenarios:
+ *
+ *
+ * type: object
+ * additionalProperties: false
+ * properties:
+ * name:
+ * type: string
+ * address:
+ * type: string
+ *
+ * @param schema the OAS schema
+ * @return true if the specified schema is an Object schema.
+ */
+ public static boolean isObjectSchema(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+
+ return (schema instanceof ObjectSchema) ||
+ // must not be a map
+ (SchemaTypeUtil.OBJECT_TYPE.equals(getType(schema)) && !(ModelUtils.isMapSchema(schema))) ||
+ // must have at least one property
+ (getType(schema) == null && schema.getProperties() != null && !schema.getProperties().isEmpty());
+ }
+
+ /**
+ * Return true if the specified schema is composed, i.e. if it uses
+ * 'oneOf', 'anyOf' or 'allOf'.
+ *
+ * @param schema the OAS schema
+ * @return true if the specified schema is a Composed schema.
+ */
+ public static boolean isComposedSchema(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+
+ // in 3.0, ComposeSchema is used for anyOf/oneOf/allOf
+ // in 3.1, it's not the case so we need more checks below
+ if (schema instanceof ComposedSchema) {
+ return true;
+ }
+
+ // has oneOf
+ if (schema.getOneOf() != null) {
+ return true;
+ }
+
+ // has anyOf
+ if (schema.getAnyOf() != null) {
+ return true;
+ }
+
+ // has allOf
+ if (schema.getAllOf() != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return true if the specified 'schema' is an object that can be extended with additional properties.
+ * Additional properties means a Schema should support all explicitly defined properties plus any
+ * undeclared properties.
+ *
+ * A MapSchema differs from an ObjectSchema in the following way:
+ * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties.
+ * - A MapSchema is an object that can be extended with an arbitrary set of properties.
+ * The payload may include dynamic properties.
+ *
+ * Note that isMapSchema returns true for a composed schema (allOf, anyOf, oneOf) that also defines
+ * additionalproperties.
+ *
+ * For example, an OpenAPI schema is considered a MapSchema in the following scenarios:
+ *
+ *
+ * type: object
+ * additionalProperties: true
+ *
+ * type: object
+ * additionalProperties:
+ * type: object
+ * properties:
+ * code:
+ * type: integer
+ *
+ * allOf:
+ * - $ref: '#/components/schemas/Class1'
+ * - $ref: '#/components/schemas/Class2'
+ * additionalProperties: true
+ *
+ * @param schema the OAS schema
+ * @return true if the specified schema is a Map schema.
+ */
+ public static boolean isMapSchema(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+
+ // additionalProperties explicitly set to false
+ if ((schema.getAdditionalProperties() instanceof Boolean && Boolean.FALSE.equals(schema.getAdditionalProperties())) ||
+ (schema.getAdditionalProperties() instanceof Schema && Boolean.FALSE.equals(((Schema) schema.getAdditionalProperties()).getBooleanSchemaValue()))
+ ) {
+ return false;
+ }
+
+ if (schema instanceof JsonSchema) { // 3.1 spec
+ return ((schema.getAdditionalProperties() instanceof Schema) ||
+ (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()));
+ } else { // 3.0 or 2.x spec
+ return (schema instanceof MapSchema) ||
+ (schema.getAdditionalProperties() instanceof Schema) ||
+ (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties());
+ }
+ }
+
+ /**
+ * Return true if the specified schema is an array of items.
+ *
+ * @param schema the OAS schema
+ * @return true if the specified schema is an Array schema.
+ */
+ public static boolean isArraySchema(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+ return (schema instanceof ArraySchema) || "array".equals(getType(schema));
+ }
+
+ /**
+ * Return the schema in the array's item. Null if schema is not an array.
+ *
+ * @param schema the OAS schema
+ * @return item schema.
+ */
+ public static Schema> getSchemaItems(Schema schema) {
+ if (!isArraySchema(schema)) {
+ return null;
+ }
+
+ Schema> items = schema.getItems();
+ if (items == null) {
+ if (schema instanceof JsonSchema) { // 3.1 spec
+ // set the items to a new schema (any type)
+ items = new Schema<>();
+ schema.setItems(items);
+ } else { // 3.0 spec, default to string
+ 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;
+ }
+
+ public static boolean isSet(Schema schema) {
+ return ModelUtils.isArraySchema(schema) && Boolean.TRUE.equals(schema.getUniqueItems());
+ }
+
+
+ public static boolean isStringSchema(Schema schema) {
+ return schema instanceof StringSchema || SchemaTypeUtil.STRING_TYPE.equals(getType(schema));
+ }
+
+ public static boolean isIntegerSchema(Schema schema) {
+ return schema instanceof IntegerSchema || SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema));
+ }
+
+ public static boolean isShortSchema(Schema schema) {
+ // format: short (int32)
+ return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) // type: integer
+ && SchemaTypeUtil.INTEGER32_FORMAT.equals(schema.getFormat());
+ }
+
+ public static boolean isLongSchema(Schema schema) {
+ // format: long (int64)
+ return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) // type: integer
+ && SchemaTypeUtil.INTEGER64_FORMAT.equals(schema.getFormat());
+ }
+
+
+ public static boolean isBooleanSchema(Schema schema) {
+ return schema instanceof BooleanSchema || SchemaTypeUtil.BOOLEAN_TYPE.equals(getType(schema));
+ }
+
+ public static boolean isNumberSchema(Schema schema) {
+ return schema instanceof NumberSchema || SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema));
+ }
+
+ public static boolean isFloatSchema(Schema schema) {
+ // format: float
+ return SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.FLOAT_FORMAT.equals(schema.getFormat());
+ }
+
+ public static boolean isDoubleSchema(Schema schema) {
+ // format: double
+ return SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.DOUBLE_FORMAT.equals(schema.getFormat());
+ }
+
+ public static boolean isDateSchema(Schema schema) {
+ return (schema instanceof DateSchema) ||
+ // format: date
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.DATE_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isDateTimeSchema(Schema schema) {
+ return (schema instanceof DateTimeSchema) ||
+ // format: date-time
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.DATE_TIME_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isPasswordSchema(Schema schema) {
+ return (schema instanceof PasswordSchema) ||
+ // double
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.PASSWORD_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isByteArraySchema(Schema schema) {
+ return (schema instanceof ByteArraySchema) ||
+ // format: byte
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.BYTE_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isBinarySchema(Schema schema) {
+ return (schema instanceof BinarySchema) ||
+ // format: binary
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.BINARY_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isFileSchema(Schema schema) {
+ return (schema instanceof FileSchema) ||
+ // file type in oas2 mapped to binary in oas3
+ isBinarySchema(schema);
+ }
+
+ public static boolean isUUIDSchema(Schema schema) {
+ return (schema instanceof UUIDSchema) ||
+ // format: uuid
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.UUID_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isURISchema(Schema schema) {
+ // format: uri
+ return SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && URI_FORMAT.equals(schema.getFormat());
+ }
+
+ public static boolean isEnumSchema(final Schema> schema) {
+ // MyEnum:
+ // type: string
+ // enum:
+ // - ENUM_1
+ // - ENUM_2
+ return schema.getEnum() != null
+ && !schema.getEnum().isEmpty();
+ }
+
+ public static boolean isEmailSchema(Schema schema) {
+ return (schema instanceof EmailSchema) ||
+ // format: email
+ (SchemaTypeUtil.STRING_TYPE.equals(getType(schema))
+ && SchemaTypeUtil.EMAIL_FORMAT.equals(schema.getFormat()));
+ }
+
+ public static boolean isDecimalSchema(Schema schema) {
+ // format: number
+ return SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) // type: string
+ && "number".equals(schema.getFormat());
+ }
+
+
+
+
+
+ /**
+ * Check to see if the schema is a model with properties only (non-composed model)
+ *
+ * @param schema potentially containing a '$ref'
+ * @return true if it's a model with at least one properties
+ */
+ public static boolean isModelWithPropertiesOnly(Schema schema) {
+ return (schema != null) &&
+ // has properties
+ (null != schema.getProperties() && !schema.getProperties().isEmpty()) &&
+ // no additionalProperties is set
+ (schema.getAdditionalProperties() == null ||
+ // additionalProperties is boolean and set to false
+ (schema.getAdditionalProperties() instanceof Boolean && !(Boolean) schema.getAdditionalProperties()) ||
+ // additionalProperties is a schema with its boolean value set to false
+ (schema.getAdditionalProperties() instanceof Schema &&
+ ((Schema) schema.getAdditionalProperties()).getBooleanSchemaValue() != null &&
+ !((Schema) schema.getAdditionalProperties()).getBooleanSchemaValue())
+ );
+ }
+
+
+ /**
+ * Check to see if the schema is a free form object.
+ *
+ * A free form object is an object (i.e. 'type: object' in a OAS document) that:
+ * 1) Does not define properties, and
+ * 2) Is not a composed schema (no anyOf, oneOf, allOf), and
+ * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}.
+ *
+ * Examples:
+ *
+ * components:
+ * schemas:
+ * arbitraryObject:
+ * type: object
+ * description: This is a free-form object.
+ * The value must be a map of strings to values. The value cannot be 'null'.
+ * It cannot be array, string, integer, number.
+ * arbitraryNullableObject:
+ * type: object
+ * description: This is a free-form object.
+ * The value must be a map of strings to values. The value can be 'null',
+ * It cannot be array, string, integer, number.
+ * nullable: true
+ * arbitraryTypeValue:
+ * description: This is NOT a free-form object.
+ * The value can be any type except the 'null' value.
+ *
+ * @param schema potentially containing a '$ref'
+ * @param openAPI document containing the Schema.
+ * @return true if it's a free-form object
+ */
+ public static boolean isFreeFormObject(Schema schema, OpenAPI openAPI) {
+ if (schema == null) {
+ // TODO: Is this message necessary? A null schema is not a free-form object, so the result is correct.
+ once(LOGGER).error("Schema cannot be null in isFreeFormObject check");
+ return false;
+ }
+
+ if (schema instanceof JsonSchema) { // 3.1 spec
+ if (isComposedSchema(schema)) { // composed schema, e.g. allOf, oneOf, anyOf
+ return false;
+ }
+
+ if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // has properties
+ return false;
+ }
+
+ if (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()) {
+ return true;
+ } else if (schema.getAdditionalProperties() instanceof JsonSchema) {
+ return true;
+ } else if (schema.getTypes() != null) {
+ if (schema.getTypes().size() == 1) { // types = [object]
+ return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getTypes().iterator().next());
+ } else { // has more than 1 type, e.g. types = [integer, string]
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ // 3.0.x spec or 2.x spec
+ // not free-form if allOf, anyOf, oneOf is not empty
+ if (isComposedSchema(schema)) {
+ List interfaces = ModelUtils.getInterfaces(schema);
+ if (interfaces != null && !interfaces.isEmpty()) {
+ return false;
+ }
+ }
+
+ // has at least one property
+ if ("object".equals(getType(schema))) {
+ // no properties
+ if ((schema.getProperties() == null || schema.getProperties().isEmpty())) {
+ Schema addlProps = ModelUtils.getAdditionalProperties(schema);
+
+ if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) {
+ // User has hard-coded vendor extension to handle free-form evaluation.
+ boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit)));
+ if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) {
+ once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit));
+ }
+ return isFreeFormExplicit;
+ }
+
+ // additionalProperties not defined
+ if (addlProps == null) {
+ return true;
+ } else {
+ addlProps = getReferencedSchema(openAPI, addlProps);
+
+ if (addlProps instanceof ObjectSchema) {
+ ObjectSchema objSchema = (ObjectSchema) addlProps;
+ // additionalProperties defined as {}
+ return objSchema.getProperties() == null || objSchema.getProperties().isEmpty();
+ } else if (addlProps instanceof Schema) {
+ // additionalProperties defined as {}
+ return addlProps.getType() == null && addlProps.get$ref() == null && (addlProps.getProperties() == null || addlProps.getProperties().isEmpty());
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * If a Schema contains a reference to another Schema with '$ref', returns the referenced Schema if it is found or the actual Schema in the other cases.
+ *
+ * @param openAPI specification being checked
+ * @param schema potentially containing a '$ref'
+ * @return schema without '$ref'
+ */
+ public static Schema> getReferencedSchema(OpenAPI openAPI, Schema schema) {
+ if (schema == null) {
+ return null;
+ }
+
+ if (StringUtils.isEmpty(schema.get$ref())) {
+ return schema;
+ }
+
+ try {
+ Schema refSchema = getSchemaFromRefToSchemaWithProperties(openAPI, schema.get$ref());
+ if (refSchema != null) {
+ // it's ref to schema's properties, #/components/schemas/Pet/properties/category for example
+ return refSchema;
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Failed to parse $ref {}. Please report the issue to openapi-generator GitHub repo.", schema.get$ref());
+ }
+
+ // a simple ref, e.g. #/components/schemas/Pet
+ String name = getSimpleRef(schema.get$ref());
+ Schema referencedSchema = getSchema(openAPI, name);
+ if (referencedSchema != null) {
+ return referencedSchema;
+ }
+
+ return schema;
+ }
+
+ /**
+ * Get the schema referenced by $ref to schema's properties, e.g. #/components/schemas/Pet/properties/category.
+ *
+ * @param openAPI specification being checked
+ * @param refString schema reference
+ * @return schema
+ */
+ public static Schema getSchemaFromRefToSchemaWithProperties(OpenAPI openAPI, String refString) {
+ if (refString == null) {
+ return null;
+ }
+
+ String[] parts = refString.split("/");
+ // #/components/schemas/Pet/properties/category
+ if (parts.length == 6 && "properties".equals(parts[4])) {
+ Schema referencedSchema = getSchema(openAPI, parts[3]); // parts[3] is Pet
+ return (Schema) referencedSchema.getProperties().get(parts[5]); // parts[5] is category
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if $ref to a reference to schema's properties, e.g. #/components/schemas/Pet/properties/category.
+ *
+ * @param refString schema reference
+ * @return true if $ref to a reference to schema's properties
+ */
+ public static boolean isRefToSchemaWithProperties(String refString) {
+ if (refString == null) {
+ return false;
+ }
+
+ String[] parts = refString.split("/");
+ // #/components/schemas/Pet/properties/category
+ if (parts.length == 6 && "properties".equals(parts[4])) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static Schema getSchema(OpenAPI openAPI, String name) {
+ if (name == null) {
+ return null;
+ }
+
+ return getSchemas(openAPI).get(name);
+ }
+
+ /**
+ * Return a Map of the schemas defined under /components/schemas in the OAS document.
+ * The returned Map only includes the direct children of /components/schemas in the OAS document; the Map
+ * does not include inlined schemas.
+ *
+ * @param openAPI the OpenAPI document.
+ * @return a map of schemas in the OAS document.
+ */
+ public static Map getSchemas(OpenAPI openAPI) {
+ if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) {
+ return openAPI.getComponents().getSchemas();
+ }
+ return Collections.emptyMap();
+ }
+
+
+ /**
+ * If a RequestBody contains a reference to another RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases.
+ *
+ * @param openAPI specification being checked
+ * @param requestBody potentially containing a '$ref'
+ * @return requestBody without '$ref'
+ */
+ public static RequestBody getReferencedRequestBody(OpenAPI openAPI, RequestBody requestBody) {
+ if (requestBody != null && StringUtils.isNotEmpty(requestBody.get$ref())) {
+ String name = getSimpleRef(requestBody.get$ref());
+ RequestBody referencedRequestBody = getRequestBody(openAPI, name);
+ if (referencedRequestBody != null) {
+ return referencedRequestBody;
+ }
+ }
+ return requestBody;
+ }
+
+ public static RequestBody getRequestBody(OpenAPI openAPI, String name) {
+ if (name == null) {
+ return null;
+ }
+
+ if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getRequestBodies() != null) {
+ return openAPI.getComponents().getRequestBodies().get(name);
+ }
+ return null;
+ }
+
+ /**
+ * If a ApiResponse contains a reference to another ApiResponse with '$ref', returns the referenced ApiResponse if it is found or the actual ApiResponse in the other cases.
+ *
+ * @param openAPI specification being checked
+ * @param apiResponse potentially containing a '$ref'
+ * @return apiResponse without '$ref'
+ */
+ public static ApiResponse getReferencedApiResponse(OpenAPI openAPI, ApiResponse apiResponse) {
+ if (apiResponse != null && StringUtils.isNotEmpty(apiResponse.get$ref())) {
+ String name = getSimpleRef(apiResponse.get$ref());
+ ApiResponse referencedApiResponse = getApiResponse(openAPI, name);
+ if (referencedApiResponse != null) {
+ return referencedApiResponse;
+ }
+ }
+ return apiResponse;
+ }
+
+ public static ApiResponse getApiResponse(OpenAPI openAPI, String name) {
+ if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null) {
+ return openAPI.getComponents().getResponses().get(name);
+ }
+ return null;
+ }
+
+ /**
+ * If a Parameter contains a reference to another Parameter with '$ref', returns the referenced Parameter if it is found or the actual Parameter in the other cases.
+ *
+ * @param openAPI specification being checked
+ * @param parameter potentially containing a '$ref'
+ * @return parameter without '$ref'
+ */
+ public static Parameter getReferencedParameter(OpenAPI openAPI, Parameter parameter) {
+ if (parameter != null && StringUtils.isNotEmpty(parameter.get$ref())) {
+ String name = getSimpleRef(parameter.get$ref());
+ Parameter referencedParameter = getParameter(openAPI, name);
+ if (referencedParameter != null) {
+ return referencedParameter;
+ }
+ }
+ return parameter;
+ }
+
+ public static Parameter getParameter(OpenAPI openAPI, String name) {
+ if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getParameters() != null) {
+ return openAPI.getComponents().getParameters().get(name);
+ }
+ return null;
+ }
+
+ /**
+ * If a Callback contains a reference to another Callback with '$ref', returns the referenced Callback if it is found or the actual Callback in the other cases.
+ *
+ * @param openAPI specification being checked
+ * @param callback potentially containing a '$ref'
+ * @return callback without '$ref'
+ */
+ public static Callback getReferencedCallback(OpenAPI openAPI, Callback callback) {
+ if (callback != null && StringUtils.isNotEmpty(callback.get$ref())) {
+ String name = getSimpleRef(callback.get$ref());
+ Callback referencedCallback = getCallback(openAPI, name);
+ if (referencedCallback != null) {
+ return referencedCallback;
+ }
+ }
+ return callback;
+ }
+
+ public static Callback getCallback(OpenAPI openAPI, String name) {
+ if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getCallbacks() != null) {
+ return openAPI.getComponents().getCallbacks().get(name);
+ }
+ return null;
+ }
+
+
+
+ /**
+ * Has self reference?
+ *
+ * @param openAPI OpenAPI spec.
+ * @param schema Schema
+ * @param visitedSchemaNames A set of visited schema names
+ * @return boolean true if it has at least one self reference
+ */
+ public static boolean hasSelfReference(OpenAPI openAPI,
+ Schema schema,
+ Set visitedSchemaNames) {
+ if (visitedSchemaNames == null) {
+ visitedSchemaNames = new HashSet();
+ }
+
+ if (schema.get$ref() != null) {
+ String ref = getSimpleRef(schema.get$ref());
+ if (!visitedSchemaNames.contains(ref)) {
+ visitedSchemaNames.add(ref);
+ Schema referencedSchema = getSchemas(openAPI).get(ref);
+ if (referencedSchema != null) {
+ return hasSelfReference(openAPI, referencedSchema, visitedSchemaNames);
+ } else {
+ LOGGER.error("Failed to obtain schema from `{}` in self reference check", ref);
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+ if (isComposedSchema(schema)) {
+ List oneOf = schema.getOneOf();
+ if (oneOf != null) {
+ for (Schema s : oneOf) {
+ if (hasSelfReference(openAPI, s, visitedSchemaNames)) {
+ return true;
+ }
+ }
+ }
+ List allOf = schema.getAllOf();
+ if (allOf != null) {
+ for (Schema s : allOf) {
+ if (hasSelfReference(openAPI, s, visitedSchemaNames)) {
+ return true;
+ }
+ }
+ }
+ List anyOf = schema.getAnyOf();
+ if (anyOf != null) {
+ for (Schema s : anyOf) {
+ if (hasSelfReference(openAPI, s, visitedSchemaNames)) {
+ return true;
+ }
+ }
+ }
+ } else if (isArraySchema(schema)) {
+ Schema itemsSchema = ModelUtils.getSchemaItems(schema);
+ if (itemsSchema != null) {
+ return hasSelfReference(openAPI, itemsSchema, visitedSchemaNames);
+ }
+ } else if (isMapSchema(schema)) {
+ Object additionalProperties = schema.getAdditionalProperties();
+ if (additionalProperties instanceof Schema) {
+ return hasSelfReference(openAPI, (Schema) additionalProperties, visitedSchemaNames);
+ }
+ } else if (schema.getNot() != null) {
+ return hasSelfReference(openAPI, schema.getNot(), visitedSchemaNames);
+ } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
+ // go through properties to see if there's any self-reference
+ for (Schema property : ((Map) schema.getProperties()).values()) {
+ if (hasSelfReference(openAPI, property, visitedSchemaNames)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned.
+ *
+ * @param openAPI OpenAPI document containing the schemas.
+ * @param schema schema (alias or direct reference)
+ * @param schemaMappings mappings of external types to be omitted by unaliasing
+ * @return actual schema
+ */
+ public static Schema unaliasSchema(OpenAPI openAPI,
+ Schema schema,
+ Map schemaMappings) {
+ Map allSchemas = getSchemas(openAPI);
+ if (allSchemas == null || allSchemas.isEmpty()) {
+ // skip the warning as the spec can have no model defined
+ //LOGGER.warn("allSchemas cannot be null/empty in unaliasSchema. Returned 'schema'");
+ return schema;
+ }
+
+ if (schema != null && StringUtils.isNotEmpty(schema.get$ref())) {
+ String simpleRef = ModelUtils.getSimpleRef(schema.get$ref());
+ if (schemaMappings.containsKey(simpleRef)) {
+ LOGGER.debug("Schema unaliasing of {} omitted because aliased class is to be mapped to {}", simpleRef, schemaMappings.get(simpleRef));
+ return schema;
+ }
+ Schema ref = allSchemas.get(simpleRef);
+ if (ref == null) {
+ if (!isRefToSchemaWithProperties(schema.get$ref())) {
+ once(LOGGER).warn("{} is not defined", schema.get$ref());
+ }
+ return schema;
+ } else if (isEnumSchema(ref)) {
+ // top-level enum class
+ return schema;
+ } else if (isArraySchema(ref)) {
+ if (isGenerateAliasAsModel(ref)) {
+ return schema; // generate a model extending array
+ } else {
+ return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
+ schemaMappings);
+ }
+ } else if (isComposedSchema(ref)) {
+ return schema;
+ } else if (isMapSchema(ref)) {
+ if (ref.getProperties() != null && !ref.getProperties().isEmpty()) // has at least one property
+ return schema; // treat it as model
+ else {
+ if (isGenerateAliasAsModel(ref)) {
+ return schema; // generate a model extending map
+ } else {
+ // treat it as a typical map
+ return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
+ schemaMappings);
+ }
+ }
+ } else if (isObjectSchema(ref)) { // model
+ if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property
+ // TODO we may need to check `hasSelfReference(openAPI, ref)` as a special/edge case:
+ // TODO we may also need to revise below to return `ref` instead of schema
+ // which is the last reference to the actual model/object
+ return schema;
+ } else { // free form object (type: object)
+ return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())),
+ schemaMappings);
+ }
+ } else {
+ return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings);
+ }
+ }
+ return schema;
+ }
+
+ /**
+ * Returns the additionalProperties Schema for the specified input schema.
+ *
+ * The additionalProperties keyword is used to control the handling of additional, undeclared
+ * properties, that is, properties whose names are not listed in the properties keyword.
+ * The additionalProperties keyword may be either a boolean or an object.
+ * If additionalProperties is a boolean and set to false, no additional properties are allowed.
+ * By default when the additionalProperties keyword is not specified in the input schema,
+ * any additional properties are allowed. This is equivalent to setting additionalProperties
+ * to the boolean value True or setting additionalProperties: {}
+ *
+ * @param schema the input schema that may or may not have the additionalProperties keyword.
+ * @return the Schema of the additionalProperties. The null value is returned if no additional
+ * properties are allowed.
+ */
+ public static Schema getAdditionalProperties(Schema schema) {
+ Object addProps = schema.getAdditionalProperties();
+ if (addProps instanceof Schema) {
+ return (Schema) addProps;
+ }
+ if (addProps == null) {
+ // When reaching this code path, this should indicate the 'additionalProperties' keyword is
+ // not present in the OAS schema. This is true for OAS 3.0 documents.
+ // However, the parsing logic is broken for OAS 2.0 documents because of the
+ // https://github.com/swagger-api/swagger-parser/issues/1369 issue.
+ // When OAS 2.0 documents are parsed, the swagger-v2-converter ignores the 'additionalProperties'
+ // keyword if the value is boolean. That means codegen is unable to determine whether
+ // additional properties are allowed or not.
+ //
+ // The original behavior was to assume additionalProperties had been set to false.
+ if (isDisallowAdditionalPropertiesIfNotPresent()) {
+ // If the 'additionalProperties' keyword is not present in a OAS schema,
+ // interpret as if the 'additionalProperties' keyword had been set to false.
+ // This is NOT compliant with the JSON schema specification. It is the original
+ // 'openapi-generator' behavior.
+ return null;
+ }
+ /*
+ // The disallowAdditionalPropertiesIfNotPresent CLI option has been set to true,
+ // but for now that only works with OAS 3.0 documents.
+ // The new behavior does not work with OAS 2.0 documents.
+ if (extensions == null || !extensions.containsKey(EXTENSION_OPENAPI_DOC_VERSION)) {
+ // Fallback to the legacy behavior.
+ return null;
+ }
+ // Get original swagger version from OAS extension.
+ // Note openAPI.getOpenapi() is always set to 3.x even when the document
+ // is converted from a OAS/Swagger 2.0 document.
+ // https://github.com/swagger-api/swagger-parser/pull/1374
+ SemVer version = new SemVer((String)extensions.get(EXTENSION_OPENAPI_DOC_VERSION));
+ if (version.major != 3) {
+ return null;
+ }
+ */
+ }
+ if (addProps == null || (addProps instanceof Boolean && (Boolean) addProps)) {
+ // Return an empty schema as the properties can take on any type per
+ // the spec. See
+ // https://github.com/OpenAPITools/openapi-generator/issues/9282 for
+ // more details.
+ return new Schema();
+ }
+ return null;
+ }
+
+ public static Header getReferencedHeader(OpenAPI openAPI, Header header) {
+ if (header != null && StringUtils.isNotEmpty(header.get$ref())) {
+ String name = getSimpleRef(header.get$ref());
+ Header referencedheader = getHeader(openAPI, name);
+ if (referencedheader != null) {
+ return referencedheader;
+ }
+ }
+ return header;
+ }
+
+ public static Header getHeader(OpenAPI openAPI, String name) {
+ if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getHeaders() != null) {
+ return openAPI.getComponents().getHeaders().get(name);
+ }
+ return null;
+ }
+
+ public static Map> getChildrenMap(OpenAPI openAPI) {
+ Map allSchemas = getSchemas(openAPI);
+
+ Map>> groupedByParent = allSchemas.entrySet().stream()
+ .filter(entry -> isComposedSchema(entry.getValue()))
+ .filter(entry -> getParentName((Schema) entry.getValue(), allSchemas) != null)
+ .collect(Collectors.groupingBy(entry -> getParentName((Schema) entry.getValue(), allSchemas)));
+
+ return groupedByParent.entrySet().stream()
+ .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream().map(e -> e.getKey()).collect(Collectors.toList())));
+ }
+
+ /**
+ * Get the interfaces from the schema (composed)
+ *
+ * @param composed schema (alias or direct reference)
+ * @return a list of schema defined in allOf, anyOf or oneOf
+ */
+ public static List getInterfaces(Schema composed) {
+ if (composed.getAllOf() != null && !composed.getAllOf().isEmpty()) {
+ return composed.getAllOf();
+ } else if (composed.getAnyOf() != null && !composed.getAnyOf().isEmpty()) {
+ return composed.getAnyOf();
+ } else if (composed.getOneOf() != null && !composed.getOneOf().isEmpty()) {
+ return composed.getOneOf();
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get the parent model name from the composed schema (allOf, anyOf, oneOf).
+ * It traverses the OAS model (possibly resolving $ref) to determine schemas
+ * that specify a determinator.
+ * If there are multiple elements in the composed schema and it is not clear
+ * which one should be the parent, return null.
+ *
+ * For example, given the following OAS spec, the parent of 'Dog' is Animal
+ * because 'Animal' specifies a discriminator.
+ *
+ * animal:
+ * type: object
+ * discriminator:
+ * propertyName: type
+ * properties:
+ * type: string
+ *
+ *
+ * dog:
+ * allOf:
+ * - $ref: '#/components/schemas/animal'
+ * - type: object
+ * properties:
+ * breed: string
+ *
+ * @param composedSchema schema (alias or direct reference)
+ * @param allSchemas all schemas
+ * @return the name of the parent model
+ */
+ public static String getParentName(Schema composedSchema, Map allSchemas) {
+ List interfaces = getInterfaces(composedSchema);
+ int nullSchemaChildrenCount = 0;
+ boolean hasAmbiguousParents = false;
+ List refedWithoutDiscriminator = new ArrayList<>();
+
+ if (interfaces != null && !interfaces.isEmpty()) {
+ List parentNameCandidates = new ArrayList<>(interfaces.size());
+ for (Schema schema : interfaces) {
+ // get the actual schema
+ if (StringUtils.isNotEmpty(schema.get$ref())) {
+ String parentName = getSimpleRef(schema.get$ref());
+ Schema s = allSchemas.get(parentName);
+ if (s == null) {
+ LOGGER.error("Failed to obtain schema from {}", parentName);
+ parentNameCandidates.add("UNKNOWN_PARENT_NAME");
+ } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) {
+ // discriminator.propertyName is used or x-parent is used
+ parentNameCandidates.add(parentName);
+ } else {
+ // not a parent since discriminator.propertyName or x-parent is not set
+ hasAmbiguousParents = true;
+ refedWithoutDiscriminator.add(parentName);
+ }
+ } else {
+ // not a ref, doing nothing, except counting the number of times the 'null' type
+ // is listed as composed element.
+ if (ModelUtils.isNullType(schema)) {
+ // If there are two interfaces, and one of them is the 'null' type,
+ // then the parent is obvious and there is no need to warn about specifying
+ // a determinator.
+ nullSchemaChildrenCount++;
+ }
+ }
+ }
+ if (parentNameCandidates.size() > 1) {
+ // unclear which one should be the parent
+ return null;
+ } else if (parentNameCandidates.size() == 1) {
+ return parentNameCandidates.get(0);
+ }
+ if (refedWithoutDiscriminator.size() == 1 && nullSchemaChildrenCount == 1) {
+ // One schema is a $ref and the other is the 'null' type, so the parent is obvious.
+ // In this particular case there is no need to specify a discriminator.
+ hasAmbiguousParents = false;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the list of parent model names from the schemas (allOf, anyOf, oneOf).
+ *
+ * @param composedSchema schema (alias or direct reference)
+ * @param allSchemas all schemas
+ * @param includeAncestors if true, include the indirect ancestors in the return value. If false, return the direct parents.
+ * @return the name of the parent model
+ */
+ public static List getAllParentsName(Schema composedSchema, Map allSchemas, boolean includeAncestors) {
+ return getAllParentsName(composedSchema, allSchemas, includeAncestors, new HashSet<>());
+ }
+
+ // Use a set of seen names to avoid infinite recursion
+ private static List getAllParentsName(
+ Schema composedSchema, Map allSchemas, boolean includeAncestors, Set seenNames) {
+ List interfaces = getInterfaces(composedSchema);
+ List names = new ArrayList();
+
+ if (interfaces != null && !interfaces.isEmpty()) {
+ for (Schema schema : interfaces) {
+ // get the actual schema
+ if (StringUtils.isNotEmpty(schema.get$ref())) {
+ String parentName = getSimpleRef(schema.get$ref());
+ if (seenNames.contains(parentName)) {
+ continue;
+ }
+ seenNames.add(parentName);
+ Schema s = allSchemas.get(parentName);
+ if (s == null) {
+ LOGGER.error("Failed to obtain schema from {}", parentName);
+ names.add("UNKNOWN_PARENT_NAME");
+ } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) {
+ // discriminator.propertyName is used or x-parent is used
+ names.add(parentName);
+ if (includeAncestors && isComposedSchema(s)) {
+ names.addAll(getAllParentsName(s, allSchemas, true, seenNames));
+ }
+ } else {
+ // not a parent since discriminator.propertyName is not set
+ }
+ } else {
+ // not a ref, doing nothing
+ }
+ }
+ }
+
+ // ensure `allParents` always includes `parent`
+ // this is more robust than keeping logic in getParentName() and getAllParentsName() in sync
+ String parentName = getParentName(composedSchema, allSchemas);
+ if (parentName != null && !names.contains(parentName)) {
+ names.add(parentName);
+ }
+
+ return names;
+ }
+
+ private static boolean hasOrInheritsDiscriminator(Schema schema, Map allSchemas, ArrayList visitedSchemas) {
+ for (Schema s : visitedSchemas) {
+ if (s == schema) {
+ return false;
+ }
+ }
+ visitedSchemas.add(schema);
+
+ if ((schema.getDiscriminator() != null && StringUtils.isNotEmpty(schema.getDiscriminator().getPropertyName()))
+ || (isExtensionParent(schema))) { // x-parent is used
+ return true;
+ } else if (StringUtils.isNotEmpty(schema.get$ref())) {
+ String parentName = getSimpleRef(schema.get$ref());
+ Schema s = allSchemas.get(parentName);
+ if (s != null) {
+ return hasOrInheritsDiscriminator(s, allSchemas, visitedSchemas);
+ } else {
+ LOGGER.error("Failed to obtain schema from {}", parentName);
+ }
+ } else if (isComposedSchema(schema)) {
+ final List interfaces = getInterfaces(schema);
+ for (Schema i : interfaces) {
+ if (hasOrInheritsDiscriminator(i, allSchemas, visitedSchemas)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If it's a boolean, returns the value of the extension `x-parent`.
+ * If it's string, return true if it's non-empty.
+ * If the return value is `true`, the schema is a parent.
+ *
+ * @param schema Schema
+ * @return boolean
+ */
+ public static boolean isExtensionParent(Schema schema) {
+ if (schema == null || schema.getExtensions() == null) {
+ return false;
+ }
+
+ Object xParent = schema.getExtensions().get("x-parent");
+ if (xParent == null) {
+ return false;
+ } else if (xParent instanceof Boolean) {
+ return (Boolean) xParent;
+ } else if (xParent instanceof String) {
+ return StringUtils.isNotEmpty((String) xParent);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * isNullType returns true if the input schema is the 'null' type.
+ *
+ * The 'null' type is supported in OAS 3.1 and above. It is not supported
+ * in OAS 2.0 and OAS 3.0.x.
+ *
+ * For example, the "null" type could be used to specify that a value must
+ * either be null or a specified type:
+ *
+ * OptionalOrder:
+ * oneOf:
+ * - type: 'null'
+ * - $ref: '#/components/schemas/Order'
+ *
+ * @param schema the OpenAPI schema
+ * @return true if the schema is the 'null' type
+ */
+ public static boolean isNullType(Schema schema) {
+ return "null".equals(getType(schema));
+ }
+
+ /**
+ * For when a type is not defined on a schema
+ * Note: properties, additionalProperties, enums, validations, items, and composed schemas (oneOf/anyOf/allOf)
+ * can be defined or omitted on these any type schemas
+ *
+ * @param schema the schema that we are checking
+ * @return boolean
+ */
+ public static boolean isAnyType(Schema schema) {
+ // $ref is not a type, it is a keyword
+ // TODO remove the ref check here, or pass in the spec version
+ // openapi 3.1.0 specs allow ref to be adjacent to any keyword
+ // openapi 3.0.3 and earlier do not allow adjacent keywords to refs
+ return (schema.get$ref() == null && getType(schema) == null);
+ }
+
+ /**
+ * Returns true if the schema contains oneOf but
+ * no properties/allOf/anyOf defined.
+ *
+ * @param schema the schema
+ * @return true if the schema contains oneOf but no properties/allOf/anyOf defined.
+ */
+ public static boolean isOneOf(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+
+ if (hasOneOf(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty()) &&
+ (schema.getAllOf() == null || schema.getAllOf().isEmpty()) &&
+ (schema.getAnyOf() == null || schema.getAnyOf().isEmpty())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the schema contains oneOf and may or may not have
+ * properties/allOf/anyOf defined.
+ *
+ * @param schema the schema
+ * @return true if allOf is not empty
+ */
+ public static boolean hasOneOf(Schema schema) {
+ if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the schema contains anyOf but
+ * no properties/allOf/anyOf defined.
+ *
+ * @param schema the schema
+ * @return true if the schema contains oneOf but no properties/allOf/anyOf defined.
+ */
+ public static boolean isAnyOf(Schema schema) {
+ if (schema == null) {
+ return false;
+ }
+
+ if (hasAnyOf(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty()) &&
+ (schema.getAllOf() == null || schema.getAllOf().isEmpty()) &&
+ (schema.getOneOf() == null || schema.getOneOf().isEmpty())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the schema contains anyOf and may or may not have
+ * properties/allOf/oneOf defined.
+ *
+ * @param schema the schema
+ * @return true if anyOf is not empty
+ */
+ public static boolean hasAnyOf(Schema schema) {
+ if (schema != null && schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns schema type.
+ * For 3.1 spec, return the first one.
+ *
+ * @param schema the schema
+ * @return schema type
+ */
+ public static String getType(Schema schema) {
+ if (schema == null) {
+ return null;
+ }
+
+ if (schema instanceof JsonSchema) {
+ if (schema.getTypes() != null && !schema.getTypes().isEmpty()) {
+ return String.valueOf(schema.getTypes().iterator().next());
+ } else {
+ return null;
+ }
+ } else {
+ return schema.getType();
+ }
+ }
+
+
+ @FunctionalInterface
+ private interface OpenAPISchemaVisitor {
+
+ void visit(Schema schema, String mimeType);
+ }
}
diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/OnceLogger.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/OnceLogger.java
new file mode 100644
index 00000000000..ea4e707800e
--- /dev/null
+++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/v3/utils/OnceLogger.java
@@ -0,0 +1,172 @@
+package io.swagger.codegen.v3.utils;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Ticker;
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+import org.slf4j.MarkerFactory;
+import org.slf4j.ext.LoggerWrapper;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Provides calling code a way to log important messages only once, regardless of how many times the invocation has occurred.
+ * This can be used, for instance, to log a warning like "One or more schemas aren't declared" without logging that message
+ * for every time the schema is mentioned in a document.
+ *
+ * This implementation currently only supports single-argument string literal log methods (e.g. {@link Logger#debug(String)}).
+ */
+@SuppressWarnings("FieldCanBeLocal")
+public class OnceLogger extends LoggerWrapper {
+ /**
+ * Allow advanced users to modify cache size of the OnceLogger (more for performance tuning in hosted environments)
+ */
+ static final String CACHE_SIZE_PROPERTY = "org.openapitools.codegen.utils.oncelogger.cachesize";
+
+ /**
+ * Allow advanced users to disable the OnceLogger (more for performance tuning in hosted environments).
+ * This is really only useful or necessary if this implementation causes issues.
+ */
+ static final String ENABLE_ONCE_LOGGER_PROPERTY = "org.openapitools.codegen.utils.oncelogger.enabled";
+
+ /**
+ * Allow advanced users to modify cache expiration of the OnceLogger (more for performance tuning in hosted environments)
+ */
+ static final String EXPIRY_PROPERTY = "org.openapitools.codegen.utils.oncelogger.expiry";
+
+ /**
+ * Internal message cache for logger decorated with the onceler.
+ */
+ static Cache messageCountCache;
+
+ /**
+ * The fully qualified class name of the logger instance,
+ * typically the logger class, logger bridge or a logger wrapper.
+ */
+ private static final String FQCN = OnceLogger.class.getName();
+
+ /**
+ * Gets the marker instance. This can be used by supported log implementations to filter/manage logs coming from
+ * this implementation differently than others (i.e. make them stand out since they're to be logged once).
+ */
+ private static final Marker MARKER = MarkerFactory.getMarker("ONCE");
+
+ /**
+ * The allowed size of the cache.
+ */
+ private static int maxCacheSize = Integer.parseInt(GlobalSettings.getProperty(CACHE_SIZE_PROPERTY, "200"));
+
+ /**
+ * The millis to expire a cached log message.
+ */
+ private static int expireMillis = Integer.parseInt(GlobalSettings.getProperty(EXPIRY_PROPERTY, "2000"));
+
+ /**
+ * The number of allowed repetitions.
+ */
+ private static int maxRepetitions = 1;
+
+ OnceLogger(Logger logger) {
+ this(logger, FQCN);
+ }
+
+ OnceLogger(Logger logger, String fqcn) {
+ super(logger, fqcn);
+ }
+
+ static {
+ caffeineCache(Ticker.systemTicker(), expireMillis);
+ }
+
+ static void caffeineCache(Ticker ticker, int expireMillis) {
+ // Initializes a cache which holds an atomic counter of log message instances.
+ // The intent is to debounce log messages such that they occur at most [maxRepetitions] per [expireMillis].
+ messageCountCache = Caffeine.newBuilder()
+ .maximumSize(maxCacheSize)
+ .expireAfterWrite(expireMillis, TimeUnit.MILLISECONDS)
+ .ticker(ticker)
+ .build();
+ }
+
+ public static Logger once(Logger logger) {
+ try {
+ if (Boolean.parseBoolean(GlobalSettings.getProperty(ENABLE_ONCE_LOGGER_PROPERTY, "true"))) {
+ return new OnceLogger(logger);
+ }
+ } catch (Exception ex) {
+ logger.warn("Unable to wrap logger instance in OnceLogger. Falling back to non-decorated implementation, which may be noisy.");
+ }
+ return logger;
+ }
+
+ /**
+ * Delegate to the appropriate method of the underlying logger.
+ *
+ * @param msg The log message.
+ */
+ @Override
+ public void trace(String msg) {
+ if (!isTraceEnabled() || !isTraceEnabled(MARKER)) return;
+
+ if (shouldLog(msg)) super.trace(MARKER, msg);
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ private boolean shouldLog(final String msg) {
+ AtomicInteger counter = messageCountCache.get(msg, i -> new AtomicInteger(0));
+ return counter.incrementAndGet() <= maxRepetitions;
+ }
+
+ /**
+ * Delegate to the appropriate method of the underlying logger.
+ *
+ * @param msg The log message.
+ */
+ @Override
+ public void debug(String msg) {
+ if (!isDebugEnabled() || !isDebugEnabled(MARKER)) return;
+
+ if (shouldLog(msg)) super.debug(MARKER, msg);
+ }
+
+ /**
+ * Delegate to the appropriate method of the underlying logger.
+ *
+ * @param msg The log message.
+ */
+ @Override
+ public void info(String msg) {
+ if (!isInfoEnabled() || !isInfoEnabled(MARKER)) return;
+
+ if (shouldLog(msg)) super.info(MARKER, msg);
+ }
+
+ /**
+ * Delegate to the appropriate method of the underlying logger.
+ *
+ * @param msg The log message.
+ */
+ @Override
+ public void warn(String msg) {
+ if (!isWarnEnabled() || !isWarnEnabled(MARKER)) return;
+
+ if (shouldLog(msg)) super.warn(MARKER, msg);
+ }
+
+ /**
+ * Delegate to the appropriate method of the underlying logger.
+ *
+ * Use this method sparingly. If you're limiting error messages, ask yourself
+ * whether your log fits better as a warning.
+ *
+ * @param msg The log message.
+ */
+ @Override
+ public void error(String msg) {
+ if (!isErrorEnabled() || !isErrorEnabled(MARKER)) return;
+
+ if (shouldLog(msg)) super.error(MARKER, msg);
+ }
+}