io.micronaut.openapi.visitor.AbstractOpenApiEndpointVisitor Maven / Gradle / Ivy
/*
* Copyright 2017-2023 original authors
*
* 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.micronaut.openapi.visitor;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JsonNode;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanMap;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.PathMatcher;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.CookieValue;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.annotation.Headers;
import io.micronaut.http.annotation.Part;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.http.annotation.RequestBean;
import io.micronaut.http.annotation.Status;
import io.micronaut.http.annotation.UriMapping;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.http.uri.UriMatchVariable;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.PackageElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.TypedElement;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.openapi.OpenApiUtils;
import io.micronaut.openapi.annotation.OpenAPIDecorator;
import io.micronaut.openapi.annotation.OpenAPIGroup;
import io.micronaut.openapi.javadoc.JavadocDescription;
import io.micronaut.openapi.swagger.core.util.PrimitiveType;
import io.micronaut.openapi.visitor.group.EndpointGroupInfo;
import io.micronaut.openapi.visitor.group.EndpointInfo;
import io.micronaut.openapi.visitor.group.GroupProperties;
import io.micronaut.openapi.visitor.group.GroupProperties.PackageProperties;
import io.micronaut.openapi.visitor.group.RouterVersioningProperties;
import io.micronaut.openapi.visitor.security.InterceptUrlMapPattern;
import io.micronaut.openapi.visitor.security.SecurityProperties;
import io.micronaut.openapi.visitor.security.SecurityRule;
import io.swagger.v3.oas.annotations.Webhook;
import io.swagger.v3.oas.annotations.callbacks.Callbacks;
import io.swagger.v3.oas.annotations.enums.Explode;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.tags.Tags;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.CookieParameter;
import io.swagger.v3.oas.models.parameters.HeaderParameter;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.PathParameter;
import io.swagger.v3.oas.models.parameters.QueryParameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import static io.micronaut.openapi.visitor.ConfigUtils.getGroupsPropertiesMap;
import static io.micronaut.openapi.visitor.ConfigUtils.getRouterVersioningProperties;
import static io.micronaut.openapi.visitor.ConfigUtils.getSecurityProperties;
import static io.micronaut.openapi.visitor.ConfigUtils.isJsonViewEnabled;
import static io.micronaut.openapi.visitor.ConfigUtils.isOpenApiEnabled;
import static io.micronaut.openapi.visitor.ConfigUtils.isSpecGenerationEnabled;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_CHILD_OP_ID_PREFIX;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX_ADD_ALWAYS;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_CHILD_PATH;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_IS_PROCESS_PARENT_CLASS;
import static io.micronaut.openapi.visitor.ContextUtils.warn;
import static io.micronaut.openapi.visitor.ConvertUtils.MAP_TYPE;
import static io.micronaut.openapi.visitor.ElementUtils.getJsonViewClass;
import static io.micronaut.openapi.visitor.ElementUtils.isDeprecated;
import static io.micronaut.openapi.visitor.ElementUtils.isFileUpload;
import static io.micronaut.openapi.visitor.ElementUtils.isIgnoredParameter;
import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable;
import static io.micronaut.openapi.visitor.ElementUtils.isNullable;
import static io.micronaut.openapi.visitor.ElementUtils.isResponseType;
import static io.micronaut.openapi.visitor.ElementUtils.isSingleResponseType;
import static io.micronaut.openapi.visitor.GeneratorUtils.addOperationDeprecatedExtension;
import static io.micronaut.openapi.visitor.GeneratorUtils.addParameterDeprecatedExtension;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ADD_ALWAYS;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOW_EMPTY_VALUE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_ALLOW_RESERVED;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_CALLBACK_URL_EXPRESSION;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_CONTENT;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DEFAULT;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DEPRECATED;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_DESCRIPTION;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_EXAMPLE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_EXAMPLES;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_EXCLUDE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_EXPLODE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_EXTENSIONS;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_HIDDEN;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_IMPLEMENTATION;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_IN;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_MEDIA_TYPE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_METHOD;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_NAME;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_OPERATION;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_OP_ID_SUFFIX;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_PARAMETERS;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_REF;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_REF_DOLLAR;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_REQUIRED;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_RESPONSE_CODE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_SCHEMA;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_STYLE;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_VALUE;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.bindSchemaAnnotationValue;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.bindSchemaForElement;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.processSchemaProperty;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.resolveSchema;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.toValue;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.toValueMap;
import static io.micronaut.openapi.visitor.SchemaUtils.COMPONENTS_CALLBACKS_PREFIX;
import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT;
import static io.micronaut.openapi.visitor.SchemaUtils.getOperationOnPathItem;
import static io.micronaut.openapi.visitor.SchemaUtils.isIgnoredHeader;
import static io.micronaut.openapi.visitor.SchemaUtils.processExtensions;
import static io.micronaut.openapi.visitor.SchemaUtils.setOperationOnPathItem;
import static io.micronaut.openapi.visitor.SchemaUtils.setSpecVersion;
import static io.micronaut.openapi.visitor.StringUtil.CLOSE_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.DOLLAR;
import static io.micronaut.openapi.visitor.StringUtil.OPEN_BRACE;
import static io.micronaut.openapi.visitor.StringUtil.THREE_DOTS;
import static io.micronaut.openapi.visitor.Utils.DEFAULT_MEDIA_TYPES;
import static io.micronaut.openapi.visitor.Utils.getMediaType;
import static io.micronaut.openapi.visitor.Utils.resolveWebhooks;
/**
* A {@link io.micronaut.inject.visitor.TypeElementVisitor} the builds the Swagger model from Micronaut controllers at compile time.
*
* @author graemerocher
* @since 1.0
*/
public abstract class AbstractOpenApiEndpointVisitor extends AbstractOpenApiVisitor {
private static final int MAX_SUMMARY_LENGTH = 200;
protected List classTags;
protected ExternalDocumentation classExternalDocs;
/**
* Executed when a class is encountered that matches the generic class.
*
* @param element The element
* @param context The visitor context
*/
public void visitClass(ClassElement element, VisitorContext context) {
if (!isOpenApiEnabled(context) || !isSpecGenerationEnabled(context)) {
return;
}
if (ignore(element, context)) {
return;
}
incrementVisitedElements(context);
processSecuritySchemes(element, context);
processTags(element, context);
processExternalDocs(element, context);
ContextUtils.remove(MICRONAUT_INTERNAL_CHILD_PATH, context);
if (element.isAnnotationPresent(Controller.class)) {
element.stringValue(UriMapping.class).ifPresent(url -> ContextUtils.put(MICRONAUT_INTERNAL_CHILD_PATH, url, context));
String prefix = StringUtils.EMPTY_STRING;
String suffix = StringUtils.EMPTY_STRING;
boolean addAlways = true;
var apiDecoratorAnn = element.getDeclaredAnnotation(OpenAPIDecorator.class);
if (apiDecoratorAnn != null) {
prefix = apiDecoratorAnn.stringValue().orElse(StringUtils.EMPTY_STRING);
suffix = apiDecoratorAnn.stringValue(PROP_OP_ID_SUFFIX).orElse(StringUtils.EMPTY_STRING);
addAlways = apiDecoratorAnn.booleanValue(PROP_ADD_ALWAYS).orElse(true);
}
ContextUtils.put(MICRONAUT_INTERNAL_CHILD_OP_ID_PREFIX, prefix, context);
ContextUtils.put(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX, suffix, context);
ContextUtils.put(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX_ADD_ALWAYS, addAlways, context);
var superTypes = new ArrayList();
Collection parentInterfaces = element.getInterfaces();
if (element.isInterface() && !parentInterfaces.isEmpty()) {
for (ClassElement parentInterface : parentInterfaces) {
if (ClassUtils.isJavaLangType(parentInterface.getName())) {
continue;
}
superTypes.add(parentInterface);
}
} else {
element.getSuperType().ifPresent(superTypes::add);
}
if (CollectionUtils.isNotEmpty(superTypes)) {
ContextUtils.put(MICRONAUT_INTERNAL_IS_PROCESS_PARENT_CLASS, true, context);
List methods = element.getEnclosedElements(ElementQuery.ALL_METHODS);
for (MethodElement method : methods) {
visitMethod(method, context);
}
ContextUtils.remove(MICRONAUT_INTERNAL_IS_PROCESS_PARENT_CLASS, context);
}
ContextUtils.remove(MICRONAUT_INTERNAL_CHILD_OP_ID_PREFIX, context);
ContextUtils.remove(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX, context);
ContextUtils.remove(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX_ADD_ALWAYS, context);
}
ContextUtils.remove(MICRONAUT_INTERNAL_CHILD_PATH, context);
}
private void processTags(ClassElement element, VisitorContext context) {
classTags = readTags(element, context);
List userDefinedClassTags = getUserDefinedClassTags(element, context);
if (CollectionUtils.isEmpty(classTags)) {
classTags = userDefinedClassTags == null ? Collections.emptyList() : userDefinedClassTags;
} else if (userDefinedClassTags != null) {
for (Tag tag : userDefinedClassTags) {
if (!containsTag(tag.getName(), classTags)) {
classTags.add(tag);
}
}
}
}
private void processExternalDocs(ClassElement element, VisitorContext context) {
var externalDocsAnn = element.findAnnotation(io.swagger.v3.oas.annotations.ExternalDocumentation.class).orElse(null);
if (externalDocsAnn == null) {
return;
}
classExternalDocs = toValue(externalDocsAnn.getValues(), context, ExternalDocumentation.class, null);
}
private boolean containsTag(String name, List tags) {
for (var tag : tags) {
if (name.equals(tag.getName())) {
return true;
}
}
return false;
}
/**
* Returns the security requirements at method level.
*
* @param element The MethodElement.
* @param context The context.
* @return The security requirements.
*/
protected abstract List methodSecurityRequirements(MethodElement element, VisitorContext context);
/**
* Returns the servers at method level.
*
* @param element The MethodElement.
* @param context The context.
* @return The servers.
*/
protected abstract List methodServers(MethodElement element, VisitorContext context);
/**
* Returns the class tags.
*
* @param element The ClassElement.
* @param context The context.
* @return The class tags.
*/
protected abstract List getUserDefinedClassTags(ClassElement element, VisitorContext context);
/**
* Returns true if the specified element should not be processed.
*
* @param element The ClassElement.
* @param context The context.
* @return true if the specified element should not be processed.
*/
protected abstract boolean ignore(ClassElement element, VisitorContext context);
/**
* Returns true if the specified element should not be processed.
*
* @param element The ClassElement.
* @param context The context.
* @return true if the specified element should not be processed.
*/
protected abstract boolean ignore(MethodElement element, VisitorContext context);
/**
* Returns the HttpMethod of the element.
*
* @param element The MethodElement.
* @return The HttpMethod of the element.
*/
protected abstract HttpMethod httpMethod(MethodElement element);
/**
* Returns the uri paths of the element.
*
* @param element The MethodElement.
* @param context The context
* @return The uri paths of the element.
*/
protected abstract List uriMatchTemplates(MethodElement element, VisitorContext context);
/**
* Returns the consumes media types.
*
* @param element The MethodElement.
* @return The consumes media types.
*/
protected abstract List consumesMediaTypes(MethodElement element);
/**
* Returns the produces media types.
*
* @param element The MethodElement.
* @return The produces media types.
*/
protected abstract List producesMediaTypes(MethodElement element);
/**
* Returns the description for the element.
*
* @param element The MethodElement.
* @return The description for the element.
*/
protected abstract String description(MethodElement element);
private boolean hasNoBindingAnnotationOrType(TypedElement parameter) {
return !parameter.isAnnotationPresent(io.swagger.v3.oas.annotations.parameters.RequestBody.class)
&& !parameter.isAnnotationPresent(QueryValue.class)
&& !parameter.isAnnotationPresent(PathVariable.class)
&& !parameter.isAnnotationPresent(Body.class)
&& !parameter.isAnnotationPresent(Part.class)
&& !parameter.isAnnotationPresent(CookieValue.class)
&& !parameter.isAnnotationPresent(Header.class)
&& !parameter.isAnnotationPresent(RequestBean.class)
&& !isResponseType(parameter.getType());
}
/**
* Executed when a method is encountered that matches the generic element.
*
* @param element The element
* @param context The visitor context
*/
public void visitMethod(MethodElement element, VisitorContext context) {
if (!isOpenApiEnabled(context) || !isSpecGenerationEnabled(context)) {
return;
}
if (ignore(element, context)) {
return;
}
HttpMethod httpMethod = httpMethod(element);
if (httpMethod == null) {
return;
}
List matchTemplates = uriMatchTemplates(element, context);
if (CollectionUtils.isEmpty(matchTemplates)) {
return;
}
incrementVisitedElements(context);
OpenAPI openApi = Utils.resolveOpenApi(context);
JavadocDescription javadocDescription;
boolean permitsRequestBody = HttpMethod.permitsRequestBody(httpMethod);
Map> pathItemsMap = resolvePathItems(context, matchTemplates);
List consumesMediaTypes = consumesMediaTypes(element);
List producesMediaTypes = producesMediaTypes(element);
ClassElement jsonViewClass = null;
if (isJsonViewEnabled(context)) {
var jsonViewAnn = element.findAnnotation(JsonView.class).orElse(null);
if (jsonViewAnn == null) {
jsonViewAnn = element.getOwningType().findAnnotation(JsonView.class).orElse(null);
}
if (jsonViewAnn != null) {
String jsonViewClassName = jsonViewAnn.stringValue().orElse(null);
if (jsonViewClassName != null) {
jsonViewClass = ContextUtils.getClassElement(jsonViewClassName, context);
}
}
}
var webhookValue = element.getAnnotation(Webhook.class);
var webhookPair = readWebhook(webhookValue, httpMethod, context);
if (webhookPair != null) {
resolveWebhooks(openApi).put(webhookPair.getFirst(), webhookPair.getSecond());
}
for (Map.Entry> pathItemEntry : pathItemsMap.entrySet()) {
List pathItems = pathItemEntry.getValue();
Map swaggerOperations = readOperations(pathItemEntry.getKey(), httpMethod, pathItems, element, context, jsonViewClass);
for (Map.Entry operationEntry : swaggerOperations.entrySet()) {
Operation swaggerOperation = operationEntry.getValue();
ExternalDocumentation externalDocs = readExternalDocs(element, context);
if (externalDocs == null) {
externalDocs = classExternalDocs;
}
if (externalDocs != null) {
swaggerOperation.setExternalDocs(externalDocs);
}
readTags(element, context, swaggerOperation, classTags == null ? Collections.emptyList() : classTags, openApi);
readSecurityRequirements(element, pathItemEntry.getKey(), swaggerOperation, context);
readApiResponses(element, context, swaggerOperation, jsonViewClass);
readServers(element, context, swaggerOperation);
readCallbacks(element, context, swaggerOperation, jsonViewClass);
javadocDescription = getMethodDescription(element, swaggerOperation);
if (isDeprecated(element)) {
swaggerOperation.setDeprecated(true);
}
readResponse(element, context, openApi, swaggerOperation, javadocDescription, jsonViewClass);
boolean isRequestBodySchemaSet = false;
if (permitsRequestBody) {
Pair requestBodyPair = readSwaggerRequestBody(element, consumesMediaTypes, context);
RequestBody requestBody = null;
if (requestBodyPair != null) {
requestBody = requestBodyPair.getFirst();
isRequestBodySchemaSet = requestBodyPair.getSecond();
}
if (requestBody != null) {
RequestBody currentRequestBody = swaggerOperation.getRequestBody();
if (currentRequestBody != null) {
swaggerOperation.setRequestBody(SchemaUtils.mergeRequestBody(currentRequestBody, requestBody));
} else {
swaggerOperation.setRequestBody(requestBody);
}
}
}
setOperationOnPathItem(operationEntry.getKey(), httpMethod, swaggerOperation);
var queryParams = new HashMap();
var pathVariables = new HashMap();
for (UriMatchTemplate matchTemplate : matchTemplates) {
for (Map.Entry varEntry : uriVariables(matchTemplate).entrySet()) {
if (pathItemEntry.getKey().contains(OPEN_BRACE + varEntry.getKey() + CLOSE_BRACE)) {
pathVariables.put(varEntry.getKey(), varEntry.getValue());
}
if (varEntry.getValue().isQuery()) {
queryParams.put(varEntry.getKey(), varEntry.getValue());
}
}
// @Parameters declared at method level take precedence over the declared as method arguments, so we process them first
processParameterAnnotationInMethod(element, openApi, matchTemplate, httpMethod, swaggerOperation, pathVariables, context);
}
var extraBodyParameters = new ArrayList();
for (Operation operation : swaggerOperations.values()) {
processParameters(element, context, openApi, operation, javadocDescription, permitsRequestBody, pathVariables, consumesMediaTypes, extraBodyParameters, httpMethod, matchTemplates, pathItems);
processExtraBodyParameters(context, httpMethod, openApi, operation, javadocDescription, isRequestBodySchemaSet, consumesMediaTypes, extraBodyParameters);
processMicronautVersionAndGroup(operation, pathItemEntry.getKey(), httpMethod, consumesMediaTypes, producesMediaTypes, element, context);
addParamsByUriTemplate(pathItemEntry.getKey(), pathVariables, queryParams, swaggerOperation);
}
if (webhookPair != null) {
SchemaUtils.mergeOperations(getOperationOnPathItem(webhookPair.getSecond(), httpMethod), swaggerOperation);
}
}
}
}
private void addParamsByUriTemplate(String path, Map pathVariables,
Map queryParams,
Operation operation) {
// check path variables in URL template which do not map to method parameters
for (var entry : pathVariables.entrySet()) {
var varName = entry.getKey();
var pathVar = entry.getValue();
if (pathVar.isExploded()
|| !path.contains(OPEN_BRACE + varName + CLOSE_BRACE)
// skip placeholders
|| path.contains(DOLLAR + OPEN_BRACE + varName + CLOSE_BRACE)
|| isAlreadyAdded(varName, operation)) {
continue;
}
operation.addParametersItem(new Parameter()
.in(ParameterIn.PATH.toString())
.name(varName)
.required(true)
.schema(PrimitiveType.STRING.createProperty()));
}
for (var entry : queryParams.entrySet()) {
var varName = entry.getKey();
var pathVar = entry.getValue();
if (pathVar.isExploded() || isAlreadyAdded(varName, operation)) {
continue;
}
operation.addParametersItem(new Parameter()
.in(ParameterIn.QUERY.toString())
.name(varName)
.schema(PrimitiveType.STRING.createProperty()));
}
}
private boolean isAlreadyAdded(String paramName, Operation operation) {
if (CollectionUtils.isEmpty(operation.getParameters())) {
return false;
}
for (var param : operation.getParameters()) {
if (param.getName().equals(paramName)) {
return true;
}
}
return false;
}
private void processExtraBodyParameters(VisitorContext context, HttpMethod httpMethod, OpenAPI openAPI,
Operation swaggerOperation,
JavadocDescription javadocDescription,
boolean isRequestBodySchemaSet,
List consumesMediaTypes,
List extraBodyParameters) {
RequestBody requestBody = swaggerOperation.getRequestBody();
if (HttpMethod.permitsRequestBody(httpMethod) && !extraBodyParameters.isEmpty()) {
if (requestBody == null) {
requestBody = new RequestBody();
var content = new Content();
requestBody.setContent(content);
requestBody.setRequired(true);
swaggerOperation.setRequestBody(requestBody);
consumesMediaTypes = CollectionUtils.isEmpty(consumesMediaTypes) ? DEFAULT_MEDIA_TYPES : consumesMediaTypes;
consumesMediaTypes.forEach(mediaType -> {
var mt = new io.swagger.v3.oas.models.media.MediaType();
var schema = setSpecVersion(new Schema<>());
schema.setType(TYPE_OBJECT);
mt.setSchema(schema);
content.addMediaType(mediaType.toString(), mt);
});
}
}
if (requestBody != null && requestBody.getContent() != null && !extraBodyParameters.isEmpty()) {
requestBody.getContent().forEach((mediaTypeName, mediaType) -> {
var schema = mediaType.getSchema();
if (schema == null) {
schema = setSpecVersion(new Schema<>());
mediaType.setSchema(schema);
}
if (schema.get$ref() != null) {
if (isRequestBodySchemaSet) {
schema = SchemaUtils.getSchemaByRef(schema, openAPI);
} else {
var composedSchema = setSpecVersion(new ComposedSchema());
var extraBodyParametersSchema = setSpecVersion(new Schema<>());
// Composition of existing + a new schema where extra body parameters are going to be added
composedSchema.addAllOfItem(schema);
composedSchema.addAllOfItem(extraBodyParametersSchema);
schema = extraBodyParametersSchema;
mediaType.setSchema(composedSchema);
}
}
for (TypedElement parameter : extraBodyParameters) {
if (!isRequestBodySchemaSet) {
processBodyParameter(context, openAPI, javadocDescription, getMediaType(mediaTypeName), schema, parameter);
}
if (mediaTypeName.equals(MediaType.MULTIPART_FORM_DATA)) {
if (CollectionUtils.isNotEmpty(schema.getProperties())) {
for (String prop : (Set) schema.getProperties().keySet()) {
Map encodings = mediaType.getEncoding();
if (encodings == null) {
encodings = new HashMap<>();
mediaType.setEncoding(encodings);
}
// if content type doesn't set by annotation,
// we can set application/octet-stream for file upload classes
Encoding encoding = encodings.get(prop);
if (encoding == null && isFileUpload(parameter.getType())) {
encoding = new Encoding();
encodings.put(prop, encoding);
encoding.setContentType(MediaType.APPLICATION_OCTET_STREAM);
}
}
}
}
}
});
}
}
private void processParameters(MethodElement element, VisitorContext context, OpenAPI openAPI,
Operation swaggerOperation, JavadocDescription javadocDescription,
boolean permitsRequestBody,
Map pathVariables,
List consumesMediaTypes,
List extraBodyParameters,
HttpMethod httpMethod,
List matchTemplates,
List pathItems) {
if (ArrayUtils.isEmpty(element.getParameters())) {
return;
}
List swaggerParameters = swaggerOperation.getParameters();
if (CollectionUtils.isEmpty(swaggerParameters)) {
swaggerParameters = new ArrayList<>();
}
for (ParameterElement parameter : element.getParameters()) {
if (!alreadyProcessedParameter(swaggerParameters, parameter)) {
processParameter(context, openAPI, swaggerOperation, javadocDescription, permitsRequestBody, pathVariables,
consumesMediaTypes, swaggerParameters, parameter, extraBodyParameters, httpMethod, matchTemplates, pathItems);
}
}
if (CollectionUtils.isNotEmpty(swaggerParameters)) {
swaggerOperation.setParameters(swaggerParameters);
}
}
private boolean alreadyProcessedParameter(List swaggerParameters, ParameterElement parameter) {
if (CollectionUtils.isEmpty(swaggerParameters)) {
return false;
}
for (var param : swaggerParameters) {
if (param.getName().equals(parameter.getName()) && param.getIn() != null) {
return true;
}
}
return false;
}
private Map readExamples(List> exampleAnns,
Element element,
VisitorContext context) {
if (CollectionUtils.isEmpty(exampleAnns)) {
return null;
}
var result = new HashMap();
for (var exampleAnn : exampleAnns) {
try {
var exampleMap = toValueMap(exampleAnn.getValues(), context, null);
result.put((String) exampleMap.get(PROP_NAME), Utils.getJsonMapper().convertValue(exampleMap, Example.class));
} catch (Exception e) {
warn("Error reading Parameter example " + exampleAnn + " for element [" + element + "]: " + e.getMessage(), context, element);
}
}
return !result.isEmpty() ? result : null;
}
private void processParameterAnnotationInMethod(MethodElement element,
OpenAPI openAPI,
UriMatchTemplate matchTemplate,
HttpMethod httpMethod,
Operation operation,
Map pathVariables,
VisitorContext context) {
var parameterAnns = element.getDeclaredAnnotationValuesByType(io.swagger.v3.oas.annotations.Parameter.class);
for (var paramAnn : parameterAnns) {
if (paramAnn.get(PROP_HIDDEN, Boolean.class, false)) {
continue;
}
var parameter = new Parameter();
parameter.schema(setSpecVersion(new Schema<>()));
paramAnn.stringValue(PROP_NAME).ifPresent(parameter::name);
paramAnn.enumValue(PROP_IN, ParameterIn.class).ifPresent(in -> parameter.in(in.toString()));
paramAnn.stringValue(PROP_DESCRIPTION).ifPresent(parameter::description);
paramAnn.booleanValue(PROP_REQUIRED).ifPresent(value -> parameter.setRequired(value ? true : null));
paramAnn.booleanValue(PROP_DEPRECATED).ifPresent(value -> parameter.setDeprecated(value ? true : null));
paramAnn.booleanValue(PROP_ALLOW_EMPTY_VALUE).ifPresent(value -> parameter.setAllowEmptyValue(value ? true : null));
paramAnn.booleanValue(PROP_ALLOW_RESERVED).ifPresent(value -> parameter.setAllowReserved(value ? true : null));
paramAnn.stringValue(PROP_EXAMPLE).ifPresent(parameter::example);
paramAnn.stringValue(PROP_REF).ifPresent(parameter::$ref);
paramAnn.enumValue(PROP_STYLE, ParameterStyle.class).ifPresent(style -> parameter.setStyle(paramStyle(style)));
var examples = readExamples(paramAnn.getAnnotations(PROP_EXAMPLES, ExampleObject.class), element, context);
if (examples != null) {
examples.forEach(parameter::addExample);
}
if (parameter.getIn() == null) {
for (ParameterElement paramEl : element.getParameters()) {
if (!paramEl.getName().equals(parameter.getName())) {
continue;
}
if (paramEl.isAnnotationPresent(PathVariable.class)) {
parameter.setIn(ParameterIn.PATH.toString());
} else if (paramEl.isAnnotationPresent(QueryValue.class)) {
parameter.setIn(ParameterIn.QUERY.toString());
} else if (paramEl.isAnnotationPresent(CookieValue.class)) {
parameter.setIn(ParameterIn.COOKIE.toString());
} else if (paramEl.isAnnotationPresent(Header.class)) {
parameter.setIn(ParameterIn.HEADER.toString());
} else {
UriMatchVariable pathVariable = pathVariables.get(parameter.getName());
// check if this parameter is optional path variable
if (pathVariable == null) {
for (UriMatchVariable variable : matchTemplate.getVariables()) {
if (variable.getName().equals(parameter.getName()) && variable.isOptional() && !variable.isQuery() && !variable.isExploded()) {
break;
}
}
}
if (pathVariable != null && !pathVariable.isOptional() && !pathVariable.isQuery() && !pathVariable.isExploded()) {
parameter.setIn(ParameterIn.PATH.toString());
}
if (parameter.getIn() == null) {
if (httpMethod == HttpMethod.GET) {
// default to QueryValue -
// https://github.com/micronaut-projects/micronaut-openapi/issues/130
parameter.setIn(ParameterIn.QUERY.toString());
}
}
}
}
}
operation.addParametersItem(parameter);
PathItem pathItem = openAPI.getPaths().get(matchTemplate.toPathString());
setOperationOnPathItem(pathItem, httpMethod, operation);
}
}
private void processParameter(VisitorContext context, OpenAPI openAPI,
Operation swaggerOperation, JavadocDescription javadocDescription,
boolean permitsRequestBody, Map pathVariables, List consumesMediaTypes,
List swaggerParameters, TypedElement parameter,
List extraBodyParameters,
HttpMethod httpMethod,
List matchTemplates,
List pathItems) {
ClassElement parameterType = parameter.getGenericType();
if (isIgnoredParameter(parameter)) {
return;
}
if (permitsRequestBody && swaggerOperation.getRequestBody() == null) {
Pair requestBodyPair = readSwaggerRequestBody(parameter, consumesMediaTypes, context);
if (requestBodyPair != null && requestBodyPair.getFirst() != null) {
swaggerOperation.setRequestBody(requestBodyPair.getFirst());
}
}
consumesMediaTypes = CollectionUtils.isNotEmpty(consumesMediaTypes) ? consumesMediaTypes : DEFAULT_MEDIA_TYPES;
if (parameter.isAnnotationPresent(Body.class)) {
Operation existedOperation = null;
// check existed operations
for (PathItem pathItem : pathItems) {
existedOperation = getOperationOnPathItem(pathItem, httpMethod);
if (existedOperation != null) {
// swaggerOperation = existedOperation;
break;
}
}
processBody(context, openAPI, swaggerOperation, javadocDescription, permitsRequestBody,
consumesMediaTypes, parameter, parameterType);
RequestBody requestBody = swaggerOperation.getRequestBody();
if (requestBody != null && requestBody.getContent() != null) {
if (existedOperation != null) {
for (Map.Entry entry : existedOperation.getRequestBody().getContent().entrySet()) {
boolean found = false;
for (MediaType mediaType : consumesMediaTypes) {
if (entry.getKey().equals(mediaType.getName())) {
found = true;
break;
}
}
if (!found) {
continue;
}
io.swagger.v3.oas.models.media.MediaType mediaType = entry.getValue();
Schema> propertySchema = bindSchemaForElement(context, parameter, parameterType, mediaType.getSchema(), null, false);
var bodyAnn = parameter.getAnnotation(Body.class);
String bodyAnnValue = bodyAnn != null ? bodyAnn.getValue(String.class).orElse(null) : null;
if (StringUtils.isNotEmpty(bodyAnnValue)) {
var wrapperSchema = setSpecVersion(new Schema<>());
wrapperSchema.setType(TYPE_OBJECT);
if (isNotNullable(parameter)) {
wrapperSchema.addRequiredItem(bodyAnnValue);
}
wrapperSchema.addProperty(bodyAnnValue, propertySchema);
mediaType.setSchema(wrapperSchema);
}
}
}
}
return;
}
if (parameter.isAnnotationPresent(RequestBean.class)) {
processRequestBean(context, openAPI, swaggerOperation, javadocDescription, permitsRequestBody, pathVariables,
consumesMediaTypes, swaggerParameters, parameter, extraBodyParameters, httpMethod, matchTemplates, pathItems);
return;
}
Parameter newParameter = processMethodParameterAnnotation(context, swaggerOperation, permitsRequestBody,
pathVariables, parameter, extraBodyParameters, httpMethod, matchTemplates);
if (newParameter == null) {
return;
}
if (newParameter.get$ref() != null) {
addSwaggerParameter(newParameter, swaggerParameters);
return;
}
if (newParameter.getExplode() != null && newParameter.getExplode() && "query".equals(newParameter.getIn()) && !parameterType.isIterable()) {
Schema> explodedSchema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null, null);
if (explodedSchema != null) {
if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null && StringUtils.isNotEmpty(explodedSchema.get$ref())) {
explodedSchema = openAPI.getComponents().getSchemas().get(explodedSchema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length()));
}
if (CollectionUtils.isNotEmpty(explodedSchema.getProperties())) {
Map props = explodedSchema.getProperties();
for (Map.Entry entry : props.entrySet()) {
var unwrappedParameter = new QueryParameter();
if (CollectionUtils.isNotEmpty(explodedSchema.getRequired()) && explodedSchema.getRequired().contains(entry.getKey())) {
unwrappedParameter.setRequired(true);
}
unwrappedParameter.setName(entry.getKey());
unwrappedParameter.setSchema(entry.getValue());
addSwaggerParameter(unwrappedParameter, swaggerParameters);
}
}
}
} else {
if (StringUtils.isEmpty(newParameter.getName())) {
newParameter.setName(parameter.getName());
}
if (newParameter.getRequired() == null && (!isNullable(parameter) || isNotNullable(parameter))) {
newParameter.setRequired(true);
}
if (javadocDescription != null && StringUtils.isEmpty(newParameter.getDescription())) {
CharSequence desc = javadocDescription.getParameters().get(parameter.getName());
if (desc != null) {
newParameter.setDescription(desc.toString());
}
}
addSwaggerParameter(newParameter, swaggerParameters);
Schema> schema = newParameter.getSchema();
if (schema == null) {
schema = resolveSchema(openAPI, parameter, parameterType, context, consumesMediaTypes, null, null, null);
}
if (schema != null) {
schema = bindSchemaForElement(context, parameter, parameterType, schema, null, false);
newParameter.setSchema(schema);
}
}
}
private void addSwaggerParameter(Parameter newParameter, List swaggerParameters) {
if (newParameter.get$ref() != null) {
swaggerParameters.add(newParameter);
return;
}
for (Parameter swaggerParameter : swaggerParameters) {
if (newParameter.getName().equals(swaggerParameter.getName())) {
return;
}
}
swaggerParameters.add(newParameter);
}
private void processBodyParameter(VisitorContext context, OpenAPI openAPI, JavadocDescription javadocDescription,
MediaType mediaType, Schema schema, TypedElement parameter) {
var jsonViewClass = getJsonViewClass(parameter, context);
Schema> propertySchema = resolveSchema(openAPI, parameter, parameter.getType(), context, Collections.singletonList(mediaType), jsonViewClass, null, null);
if (propertySchema == null) {
return;
}
parameter.stringValue(io.swagger.v3.oas.annotations.Parameter.class, PROP_DESCRIPTION)
.ifPresent(propertySchema::setDescription);
processSchemaProperty(context, parameter, parameter.getType(), null, schema, propertySchema);
if (isNullable(parameter) && !isNotNullable(parameter)) {
// Keep null if not
SchemaUtils.setNullable(propertySchema);
}
if (javadocDescription != null && StringUtils.isEmpty(propertySchema.getDescription())) {
String doc = javadocDescription.getParameters().get(parameter.getName());
if (doc != null) {
propertySchema.setDescription(doc);
}
}
}
private Parameter processMethodParameterAnnotation(VisitorContext context, Operation swaggerOperation,
boolean permitsRequestBody,
Map pathVariables, TypedElement parameter,
List extraBodyParameters,
HttpMethod httpMethod,
List matchTemplates) {
boolean isBodyParameter = false;
Parameter newParameter = null;
String parameterName = parameter.getName();
if (!parameter.hasStereotype(Bindable.class) && pathVariables.containsKey(parameterName)) {
UriMatchVariable urlVar = pathVariables.get(parameterName);
newParameter = urlVar.isQuery() ? new QueryParameter() : new PathParameter();
newParameter.setName(parameterName);
final boolean exploded = urlVar.isExploded();
if (exploded) {
newParameter.setExplode(exploded);
}
} else if (parameter.isAnnotationPresent(PathVariable.class)) {
String paramName = parameter.getValue(PathVariable.class, String.class).orElse(parameterName);
if (paramName.isEmpty()) {
paramName = parameterName;
}
UriMatchVariable variable = pathVariables.get(paramName);
if (variable == null) {
if (!isNullable(parameter)) {
warn("Path variable name: '" + paramName + "' not found in path, operation: " + swaggerOperation.getOperationId(), context, parameter);
}
return null;
}
newParameter = new PathParameter();
newParameter.setName(paramName);
final boolean exploded = variable.isExploded();
if (exploded) {
newParameter.setExplode(true);
}
} else if (parameter.isAnnotationPresent(Header.class)) {
var headerName = getHeaderName(parameter, parameterName);
if (headerName == null) {
return null;
}
newParameter = new HeaderParameter();
newParameter.setName(headerName);
} else if (parameter.isAnnotationPresent(Headers.class)) {
var headerAnns = parameter.getAnnotationValuesByType(Header.class);
if (CollectionUtils.isNotEmpty(headerAnns)) {
var headerName = getHeaderName(parameter, parameterName);
if (headerName == null) {
return null;
}
newParameter = new HeaderParameter();
newParameter.setName(headerName);
}
} else if (parameter.isAnnotationPresent(CookieValue.class)) {
String cookieName = parameter.stringValue(CookieValue.class).orElse(parameterName);
newParameter = new CookieParameter();
newParameter.setName(cookieName);
} else if (parameter.isAnnotationPresent(QueryValue.class)) {
String queryVar = parameter.stringValue(QueryValue.class).orElse(parameterName);
newParameter = new QueryParameter();
newParameter.setName(queryVar);
} else if (parameter.isAnnotationPresent(Part.class) && permitsRequestBody) {
extraBodyParameters.add(parameter);
isBodyParameter = true;
} else if (parameter.hasAnnotation("io.micronaut.management.endpoint.annotation.Selector")) {
newParameter = new PathParameter();
newParameter.setName(parameterName);
} else if (hasNoBindingAnnotationOrType(parameter)) {
var parameterAnn = parameter.getAnnotation(io.swagger.v3.oas.annotations.Parameter.class);
// Skip recognizing parameter if it's manually defined by PROP_IN
var paramIn = parameterAnn != null ? parameterAnn.stringValue(PROP_IN).orElse(null) : null;
if (parameterAnn == null || !parameterAnn.booleanValue(PROP_HIDDEN).orElse(false)
&& (paramIn == null || paramIn.equals(ParameterIn.DEFAULT.toString()))) {
if (permitsRequestBody) {
extraBodyParameters.add(parameter);
isBodyParameter = true;
} else {
UriMatchVariable pathVariable = pathVariables.get(parameterName);
boolean isExploded = false;
// check if this parameter is optional path variable
if (pathVariable == null) {
for (UriMatchTemplate matchTemplate : matchTemplates) {
for (UriMatchVariable variable : matchTemplate.getVariables()) {
if (variable.getName().equals(parameterName)) {
isExploded = variable.isExploded();
if (variable.isOptional() && !variable.isQuery() && !isExploded) {
return null;
}
break;
}
}
}
}
if (pathVariable != null && !pathVariable.isOptional() && !pathVariable.isQuery()) {
newParameter = new PathParameter();
newParameter.setName(parameterName);
if (pathVariable.isExploded()) {
newParameter.setExplode(true);
}
}
if (newParameter == null) {
if (httpMethod == HttpMethod.GET) {
// default to QueryValue -
// https://github.com/micronaut-projects/micronaut-openapi/issues/130
newParameter = new QueryParameter();
newParameter.setName(parameterName);
}
}
if (newParameter != null && isExploded) {
newParameter.setExplode(true);
}
}
}
}
if (isBodyParameter) {
return null;
}
var paramAnn = parameter.findAnnotation(io.swagger.v3.oas.annotations.Parameter.class).orElse(null);
if (paramAnn != null) {
if (paramAnn.get(PROP_HIDDEN, Boolean.class, false)) {
// ignore hidden parameters
return null;
}
Map paramValues = toValueMap(paramAnn.getValues(), context, null);
Utils.normalizeEnumValues(paramValues, Collections.singletonMap(PROP_IN, ParameterIn.class));
if (parameter.isAnnotationPresent(Header.class)) {
paramValues.put(PROP_IN, ParameterIn.HEADER.toString());
} else if (parameter.isAnnotationPresent(CookieValue.class)) {
paramValues.put(PROP_IN, ParameterIn.COOKIE.toString());
} else if (parameter.isAnnotationPresent(QueryValue.class)) {
paramValues.put(PROP_IN, ParameterIn.QUERY.toString());
}
processExplode(paramAnn, paramValues);
JsonNode jsonNode = Utils.getJsonMapper().valueToTree(paramValues);
if (newParameter == null) {
try {
newParameter = ConvertUtils.treeToValue(jsonNode, Parameter.class, context);
if (jsonNode.has(PROP_SCHEMA)) {
JsonNode schemaNode = jsonNode.get(PROP_SCHEMA);
if (schemaNode.has(PROP_REF_DOLLAR)) {
if (newParameter == null) {
newParameter = new Parameter();
}
newParameter.schema(setSpecVersion(new Schema<>().$ref(schemaNode.get(PROP_REF_DOLLAR).asText())));
}
}
} catch (Exception e) {
warn("Error reading Swagger Parameter for element [" + parameter + "]: " + e.getMessage(), context, parameter);
}
} else {
try {
Parameter v = ConvertUtils.treeToValue(jsonNode, Parameter.class, context);
if (v == null) {
Map target = OpenApiUtils.getConvertJsonMapper().convertValue(newParameter, MAP_TYPE);
for (CharSequence name : paramValues.keySet()) {
Object o = paramValues.get(name.toString());
if (o != null) {
target.put(name.toString(), o);
}
}
newParameter = OpenApiUtils.getConvertJsonMapper().convertValue(target, Parameter.class);
} else {
// horrible hack because Swagger
// ParameterDeserializer breaks updating
// existing objects
BeanMap beanMap = BeanMap.of(v);
Map target = OpenApiUtils.getConvertJsonMapper().convertValue(newParameter, MAP_TYPE);
for (CharSequence name : beanMap.keySet()) {
Object o = beanMap.get(name.toString());
if (o != null) {
target.put(name.toString(), o);
}
}
newParameter = OpenApiUtils.getConvertJsonMapper().convertValue(target, Parameter.class);
}
} catch (IOException e) {
warn("Error reading Swagger Parameter for element [" + parameter + "]: " + e.getMessage(), context, parameter);
}
}
if (newParameter != null && newParameter.get$ref() != null) {
return newParameter;
}
if (newParameter != null) {
final Schema> parameterSchema = newParameter.getSchema();
if (paramAnn.contains(PROP_SCHEMA) && parameterSchema != null) {
paramAnn.get(PROP_SCHEMA, AnnotationValue.class)
.ifPresent(schemaAnn -> bindSchemaAnnotationValue(context, parameter, parameterSchema, schemaAnn, null));
}
}
}
if (newParameter != null && isNullable(parameter) && !isNotNullable(parameter)) {
newParameter.setRequired(null);
}
if (newParameter != null && isDeprecated(parameter)) {
newParameter.setDeprecated(true);
addParameterDeprecatedExtension(parameter, newParameter, context);
}
return newParameter;
}
private String getHeaderName(TypedElement parameter, String parameterName) {
// skip params like this: @Header Map
if (isIgnoredParameter(parameter)) {
return null;
}
String headerName = parameter.stringValue(Header.class, PROP_NAME)
.orElse(parameter.stringValue(Header.class)
.orElseGet(() -> NameUtils.hyphenate(parameterName)));
if (isIgnoredHeader(headerName)) {
return null;
}
return headerName;
}
private void processBody(VisitorContext context, OpenAPI openAPI,
Operation swaggerOperation, JavadocDescription javadocDescription,
boolean permitsRequestBody, List consumesMediaTypes, TypedElement parameter,
ClassElement parameterType) {
var jsonViewClass = getJsonViewClass(parameter, context);
if (!permitsRequestBody) {
return;
}
RequestBody requestBody = swaggerOperation.getRequestBody();
if (requestBody == null) {
requestBody = new RequestBody();
swaggerOperation.setRequestBody(requestBody);
}
if (requestBody.getDescription() == null && javadocDescription != null) {
CharSequence desc = javadocDescription.getParameters().get(parameter.getName());
if (desc != null) {
requestBody.setDescription(desc.toString());
}
}
if (requestBody.getRequired() == null && (!isNullable(parameterType) || isNotNullable(parameterType))) {
requestBody.setRequired(true);
}
final Content content = buildContent(parameter, parameterType, consumesMediaTypes, openAPI, context, jsonViewClass);
if (requestBody.getContent() == null) {
requestBody.setContent(content);
} else {
final Content currentContent = requestBody.getContent();
for (Map.Entry entry : content.entrySet()) {
io.swagger.v3.oas.models.media.MediaType mediaType = entry.getValue();
io.swagger.v3.oas.models.media.MediaType existedMediaType = currentContent.get(entry.getKey());
if (existedMediaType == null) {
currentContent.put(entry.getKey(), mediaType);
continue;
}
if (existedMediaType.getSchema() == null) {
existedMediaType.setSchema(mediaType.getSchema());
}
if (existedMediaType.getEncoding() == null) {
existedMediaType.setEncoding(mediaType.getEncoding());
}
if (existedMediaType.getExtensions() == null) {
existedMediaType.setExtensions(mediaType.getExtensions());
}
if (existedMediaType.getExamples() == null) {
existedMediaType.setExamples(mediaType.getExamples());
}
if (existedMediaType.getExample() == null && mediaType.getExampleSetFlag()) {
existedMediaType.setExample(mediaType.getExample());
}
}
}
}
private void processRequestBean(VisitorContext context, OpenAPI openAPI,
Operation swaggerOperation, JavadocDescription javadocDescription,
boolean permitsRequestBody, Map pathVariables, List consumesMediaTypes,
List swaggerParameters, TypedElement parameter,
List extraBodyParameters,
HttpMethod httpMethod,
List matchTemplates,
List pathItems) {
for (FieldElement field : parameter.getType().getFields()) {
if (field.isStatic()) {
continue;
}
processParameter(context, openAPI, swaggerOperation, javadocDescription, permitsRequestBody, pathVariables,
consumesMediaTypes, swaggerParameters, field, extraBodyParameters, httpMethod, matchTemplates, pathItems);
}
}
private void readResponse(MethodElement element, VisitorContext context, OpenAPI openAPI,
Operation swaggerOperation, JavadocDescription javadocDescription, @Nullable ClassElement jsonViewClass) {
boolean withMethodResponses = element.hasDeclaredAnnotation(io.swagger.v3.oas.annotations.responses.ApiResponses.class)
|| element.hasDeclaredAnnotation(io.swagger.v3.oas.annotations.responses.ApiResponse.class);
HttpStatus methodResponseStatus = element.enumValue(Status.class, HttpStatus.class).orElse(HttpStatus.OK);
String responseCode = Integer.toString(methodResponseStatus.getCode());
ApiResponses responses = swaggerOperation.getResponses();
ApiResponse response = null;
if (responses == null) {
responses = new ApiResponses();
swaggerOperation.setResponses(responses);
} else {
ApiResponse defaultResponse = responses.remove(PROP_DEFAULT);
response = responses.get(responseCode);
if (response == null && defaultResponse != null) {
response = defaultResponse;
responses.put(responseCode, response);
}
}
if (response == null && !withMethodResponses) {
response = new ApiResponse();
if (javadocDescription == null || StringUtils.isEmpty(javadocDescription.getReturnDescription())) {
response.setDescription(swaggerOperation.getOperationId() + StringUtils.SPACE + responseCode + " response");
} else {
response.setDescription(javadocDescription.getReturnDescription());
}
addResponseContent(element, context, openAPI, response, jsonViewClass);
responses.put(responseCode, response);
} else if (response != null && response.getContent() == null) {
addResponseContent(element, context, openAPI, response, jsonViewClass);
}
}
private void addResponseContent(MethodElement element, VisitorContext context, OpenAPI openAPI, ApiResponse response, @Nullable ClassElement jsonViewClass) {
ClassElement returnType = returnType(element, context);
if (returnType != null && !returnType.getCanonicalName().equals(Void.class.getName())) {
List producesMediaTypes = producesMediaTypes(element);
Content content;
if (producesMediaTypes.isEmpty()) {
content = buildContent(element, returnType, DEFAULT_MEDIA_TYPES, openAPI, context, jsonViewClass);
} else {
content = buildContent(element, returnType, producesMediaTypes, openAPI, context, jsonViewClass);
}
response.setContent(content);
}
}
private ClassElement returnType(MethodElement element, VisitorContext context) {
ClassElement returnType = element.getGenericReturnType();
if (ElementUtils.isVoid(returnType) || ElementUtils.isReactiveAndVoid(returnType)) {
returnType = null;
} else if (isResponseType(returnType)) {
returnType = returnType.getFirstTypeArgument().orElse(returnType);
} else if (isSingleResponseType(returnType)) {
returnType = returnType.getFirstTypeArgument().orElse(null);
if (returnType != null) {
returnType = returnType.getFirstTypeArgument().orElse(returnType);
}
}
return returnType;
}
private Map uriVariables(UriMatchTemplate matchTemplate) {
List pv = matchTemplate.getVariables();
var pathVariables = new LinkedHashMap(pv.size());
for (UriMatchVariable variable : pv) {
pathVariables.put(variable.getName(), variable);
}
return pathVariables;
}
private JavadocDescription getMethodDescription(MethodElement element,
Operation swaggerOperation) {
String descr = description(element);
if (StringUtils.isNotEmpty(descr) && StringUtils.isEmpty(swaggerOperation.getDescription())) {
swaggerOperation.setDescription(descr);
String summary = descr.substring(0, descr.indexOf('.') + 1);
if (summary.length() > MAX_SUMMARY_LENGTH) {
summary = summary.substring(0, MAX_SUMMARY_LENGTH) + THREE_DOTS;
}
swaggerOperation.setSummary(summary);
}
JavadocDescription javadocDescription = element.getDocumentation()
.map(Utils.getJavadocParser()::parse)
.orElse(null);
if (javadocDescription != null) {
if (StringUtils.isEmpty(swaggerOperation.getDescription()) && StringUtils.hasText(javadocDescription.getMethodDescription())) {
swaggerOperation.setDescription(javadocDescription.getMethodDescription());
}
if (StringUtils.isEmpty(swaggerOperation.getSummary()) && StringUtils.hasText(javadocDescription.getMethodSummary())) {
swaggerOperation.setSummary(javadocDescription.getMethodSummary());
}
}
return javadocDescription;
}
private Pair readWebhook(@Nullable AnnotationValue webhookAnnValue,
HttpMethod httpMethod,
VisitorContext context) {
if (webhookAnnValue == null) {
return null;
}
var name = webhookAnnValue.stringValue(PROP_NAME).orElse(null);
if (StringUtils.isEmpty(name)) {
return null;
}
var operationAnn = webhookAnnValue.getAnnotation(PROP_OPERATION, io.swagger.v3.oas.annotations.Operation.class).orElse(null);
Operation operation;
HttpMethod method;
if (operationAnn != null) {
operation = toValue(operationAnn.getValues(), context, Operation.class, null);
method = HttpMethod.parse(operationAnn.stringValue(PROP_METHOD).orElse(httpMethod.name()));
} else {
operation = new Operation();
method = HttpMethod.POST;
}
var pathItem = new PathItem();
setOperationOnPathItem(pathItem, method, operation);
return Pair.of(name, pathItem);
}
private Map readOperations(String path, HttpMethod httpMethod, List pathItems, MethodElement element, VisitorContext context, @Nullable ClassElement jsonViewClass) {
var swaggerOperations = new HashMap(pathItems.size());
var operationAnn = element.findAnnotation(io.swagger.v3.oas.annotations.Operation.class).orElse(null);
for (PathItem pathItem : pathItems) {
var swaggerOperation = operationAnn != null ? toValue(operationAnn.getValues(), context, Operation.class, jsonViewClass) : null;
if (swaggerOperation == null) {
swaggerOperation = new Operation();
}
addOperationDeprecatedExtension(element, swaggerOperation, context);
if (CollectionUtils.isNotEmpty(swaggerOperation.getParameters())) {
swaggerOperation.getParameters().removeIf(Objects::isNull);
}
ParameterElement[] methodParams = element.getParameters();
if (ArrayUtils.isNotEmpty(methodParams) && operationAnn != null) {
var paramAnns = operationAnn.getAnnotations(PROP_PARAMETERS, io.swagger.v3.oas.annotations.Parameter.class);
if (CollectionUtils.isNotEmpty(paramAnns)) {
for (ParameterElement methodParam : methodParams) {
AnnotationValue paramAnn = null;
for (AnnotationValue param : paramAnns) {
String paramName = param.stringValue(PROP_NAME).orElse(null);
if (methodParam.getName().equals(paramName)) {
paramAnn = param;
break;
}
}
Parameter swaggerParam = null;
if (paramAnn != null && !paramAnn.booleanValue(PROP_HIDDEN).orElse(false)) {
String paramName = paramAnn.stringValue(PROP_NAME).orElse(null);
if (paramName != null) {
if (CollectionUtils.isNotEmpty(swaggerOperation.getParameters())) {
for (Parameter createdParameter : swaggerOperation.getParameters()) {
if (createdParameter.getName().equals(paramName)) {
swaggerParam = createdParameter;
break;
}
}
}
}
if (swaggerParam == null) {
if (swaggerOperation.getParameters() == null) {
swaggerOperation.setParameters(new ArrayList<>());
}
swaggerParam = new Parameter();
swaggerOperation.getParameters().add(swaggerParam);
}
if (paramName != null) {
swaggerParam.setName(paramName);
}
paramAnn.stringValue(PROP_DESCRIPTION).ifPresent(swaggerParam::setDescription);
var required = paramAnn.booleanValue(PROP_REQUIRED).orElse(false);
if (required) {
swaggerParam.setRequired(true);
}
var allowEmptyValue = paramAnn.booleanValue(PROP_ALLOW_EMPTY_VALUE).orElse(false);
if (allowEmptyValue) {
swaggerParam.setAllowEmptyValue(true);
}
var allowReserved = paramAnn.booleanValue(PROP_ALLOW_RESERVED).orElse(false);
if (allowReserved) {
swaggerParam.setAllowReserved(true);
}
paramAnn.stringValue(PROP_EXAMPLE).ifPresent(swaggerParam::setExample);
var examples = readExamples(paramAnn.getAnnotations(PROP_EXAMPLES, ExampleObject.class), element, context);
if (examples != null) {
examples.forEach(swaggerParam::addExample);
}
var style = paramAnn.get(PROP_STYLE, ParameterStyle.class).orElse(ParameterStyle.DEFAULT);
if (style != ParameterStyle.DEFAULT) {
swaggerParam.setStyle(paramStyle(style));
}
paramAnn.stringValue(PROP_REF).ifPresent(swaggerParam::set$ref);
Optional inOpt = paramAnn.get(PROP_IN, ParameterIn.class);
if (inOpt.isPresent()) {
var in = inOpt.get();
if (in == ParameterIn.DEFAULT) {
swaggerParam.setIn(calcIn(path, httpMethod, methodParam));
} else {
swaggerParam.setIn(in.toString());
}
}
}
if (swaggerParam != null && StringUtils.isEmpty(swaggerParam.getIn())) {
swaggerParam.setIn(calcIn(path, httpMethod, methodParam));
}
}
}
}
String prefix;
String suffix;
boolean addAlways;
var apiDecoratorAnn = element.getDeclaredAnnotation(OpenAPIDecorator.class);
if (apiDecoratorAnn != null) {
prefix = apiDecoratorAnn.stringValue().orElse(StringUtils.EMPTY_STRING);
suffix = apiDecoratorAnn.stringValue(PROP_OP_ID_SUFFIX).orElse(StringUtils.EMPTY_STRING);
addAlways = apiDecoratorAnn.booleanValue(PROP_ADD_ALWAYS).orElse(true);
} else {
prefix = ContextUtils.get(MICRONAUT_INTERNAL_CHILD_OP_ID_PREFIX, String.class, StringUtils.EMPTY_STRING, context);
suffix = ContextUtils.get(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX, String.class, StringUtils.EMPTY_STRING, context);
addAlways = ContextUtils.get(MICRONAUT_INTERNAL_CHILD_OP_ID_SUFFIX_ADD_ALWAYS, Boolean.class, true, context);
}
if (StringUtils.isEmpty(swaggerOperation.getOperationId())) {
swaggerOperation.setOperationId(prefix + element.getName() + suffix);
} else if (addAlways) {
swaggerOperation.setOperationId(prefix + swaggerOperation.getOperationId() + suffix);
}
if (swaggerOperation.getDescription() != null && swaggerOperation.getDescription().isEmpty()) {
swaggerOperation.setDescription(null);
}
swaggerOperations.put(pathItem, swaggerOperation);
}
return swaggerOperations;
}
private String calcIn(String path, HttpMethod httpMethod, ParameterElement methodParam) {
String paramName = methodParam.getName();
Set paramAnnNames = methodParam.getAnnotationNames();
if (CollectionUtils.isNotEmpty(paramAnnNames)) {
if (paramAnnNames.contains(QueryValue.class.getName())) {
return ParameterIn.QUERY.toString();
} else if (paramAnnNames.contains(PathVariable.class.getName())) {
return ParameterIn.PATH.toString();
} else if (paramAnnNames.contains(Header.class.getName())) {
return ParameterIn.HEADER.toString();
} else if (paramAnnNames.contains(CookieValue.class.getName())) {
return ParameterIn.COOKIE.toString();
}
}
if (httpMethod == HttpMethod.GET) {
if (path.contains(OPEN_BRACE + paramName + CLOSE_BRACE)) {
return ParameterIn.PATH.toString();
} else {
return ParameterIn.QUERY.toString();
}
} else {
if (path.contains(OPEN_BRACE + paramName + CLOSE_BRACE)) {
return ParameterIn.PATH.toString();
}
}
return null;
}
private Parameter.StyleEnum paramStyle(ParameterStyle paramAnnStyle) {
if (paramAnnStyle == null) {
return null;
}
return switch (paramAnnStyle) {
case MATRIX -> Parameter.StyleEnum.MATRIX;
case LABEL -> Parameter.StyleEnum.LABEL;
case FORM -> Parameter.StyleEnum.FORM;
case SPACEDELIMITED -> Parameter.StyleEnum.SPACEDELIMITED;
case PIPEDELIMITED -> Parameter.StyleEnum.PIPEDELIMITED;
case DEEPOBJECT -> Parameter.StyleEnum.DEEPOBJECT;
case SIMPLE -> Parameter.StyleEnum.SIMPLE;
case DEFAULT -> null;
};
}
private ExternalDocumentation readExternalDocs(MethodElement element, VisitorContext context) {
var externalDocsAnn = element.findAnnotation(io.swagger.v3.oas.annotations.ExternalDocumentation.class).orElse(null);
if (externalDocsAnn == null) {
return null;
}
return toValue(externalDocsAnn.getValues(), context, ExternalDocumentation.class, null);
}
private void readSecurityRequirements(MethodElement element, String path, Operation operation, VisitorContext context) {
List securityRequirements = methodSecurityRequirements(element, context);
if (CollectionUtils.isNotEmpty(securityRequirements)) {
for (SecurityRequirement securityItem : securityRequirements) {
operation.addSecurityItem(securityItem);
}
return;
}
processMicronautSecurityConfig(element, path, operation, context);
}
private void processMicronautSecurityConfig(MethodElement element, String path, Operation operation, VisitorContext context) {
SecurityProperties securityProperties = getSecurityProperties(context);
if (!securityProperties.isEnabled()
|| !securityProperties.isMicronautSecurityEnabled()
|| (!securityProperties.isTokenEnabled()
&& !securityProperties.isJwtEnabled()
&& !securityProperties.isBasicAuthEnabled()
&& !securityProperties.isOauth2Enabled()
)) {
return;
}
OpenAPI openAPI = Utils.resolveOpenApi(context);
Components components = openAPI.getComponents();
String securitySchemeName;
if (components != null && CollectionUtils.isNotEmpty(components.getSecuritySchemes())) {
securitySchemeName = components.getSecuritySchemes().keySet().iterator().next();
} else {
if (components == null) {
components = new Components();
openAPI.setComponents(components);
}
if (components.getSecuritySchemes() == null) {
components.setSecuritySchemes(new HashMap<>());
}
securitySchemeName = securityProperties.getDefaultSchemaName();
SecurityScheme securityScheme = components.getSecuritySchemes().get(securitySchemeName);
if (securityScheme == null) {
securityScheme = new SecurityScheme();
if (securityProperties.isOauth2Enabled()) {
securityScheme.setType(SecurityScheme.Type.OAUTH2);
} else if (securityProperties.isBasicAuthEnabled()
|| securityProperties.isTokenEnabled()
|| securityProperties.isJwtEnabled()) {
securityScheme.setType(SecurityScheme.Type.HTTP);
if (securityProperties.isJwtEnabled()) {
securityScheme.setBearerFormat("JWT");
}
}
if (securityProperties.isJwtEnabled() || securityProperties.isJwtBearerEnabled()) {
securityScheme.setScheme("bearer");
} else if (securityProperties.isBasicAuthEnabled()) {
securityScheme.setScheme("basic");
}
components.addSecuritySchemes(securitySchemeName, securityScheme);
}
}
var classLevelSecuredAnn = element.getOwningType().getAnnotation("io.micronaut.security.annotation.Secured");
var methodLevelSecuredAnn = element.getAnnotation("io.micronaut.security.annotation.Secured");
List access = Collections.emptyList();
if (methodLevelSecuredAnn != null) {
access = methodLevelSecuredAnn.getValue(Argument.LIST_OF_STRING).orElse(null);
} else if (classLevelSecuredAnn != null) {
access = classLevelSecuredAnn.getValue(Argument.LIST_OF_STRING).orElse(null);
}
processSecurityAccess(securitySchemeName, access, operation);
List securityRules = securityProperties.getInterceptUrlMapPatterns();
if (CollectionUtils.isNotEmpty(securityRules)) {
HttpMethod httpMethod = httpMethod(element);
for (InterceptUrlMapPattern securityRule : securityRules) {
if (PathMatcher.ANT.matches(securityRule.getPattern(), path)
&& (httpMethod == null || securityRule.getHttpMethod() == null || httpMethod == securityRule.getHttpMethod())) {
processSecurityAccess(securitySchemeName, securityRule.getAccess(), operation);
}
}
}
}
private void processSecurityAccess(String securitySchemeName, List access, Operation operation) {
if (securitySchemeName == null || CollectionUtils.isEmpty(access)) {
return;
}
String firstAccessItem = access.get(0);
if (access.size() == 1 && (firstAccessItem.equals(SecurityRule.IS_ANONYMOUS) || firstAccessItem.equals(SecurityRule.DENY_ALL))) {
return;
}
if (access.size() == 1 && firstAccessItem.equals(SecurityRule.IS_AUTHENTICATED)) {
access = Collections.emptyList();
}
SecurityRequirement existedSecurityRequirement = null;
List existedSecList = null;
if (CollectionUtils.isNotEmpty(operation.getSecurity())) {
for (SecurityRequirement securityRequirement : operation.getSecurity()) {
if (securityRequirement.containsKey(securitySchemeName)) {
existedSecList = securityRequirement.get(securitySchemeName);
existedSecurityRequirement = securityRequirement;
break;
}
}
}
if (existedSecList != null) {
if (access.isEmpty()) {
return;
}
if (existedSecList.isEmpty()) {
existedSecurityRequirement.put(securitySchemeName, access);
} else {
var finalAccess = new HashSet<>(existedSecList);
finalAccess.addAll(access);
existedSecurityRequirement.put(securitySchemeName, new ArrayList<>(finalAccess));
}
} else {
var securityRequirement = new SecurityRequirement();
securityRequirement.put(securitySchemeName, access);
operation.addSecurityItem(securityRequirement);
}
}
private void processExplode(AnnotationValue paramAnn, Map paramValues) {
Optional explode = paramAnn.enumValue(PROP_EXPLODE, Explode.class);
if (explode.isEmpty()) {
return;
}
switch (explode.get()) {
case TRUE -> paramValues.put(PROP_EXPLODE, Boolean.TRUE);
case FALSE -> paramValues.put(PROP_EXPLODE, Boolean.FALSE);
default -> {
var in = (String) paramValues.get(PROP_IN);
if (StringUtils.isEmpty(in)) {
in = PROP_DEFAULT;
}
switch (ParameterIn.valueOf(in.toUpperCase(Locale.ENGLISH))) {
case COOKIE, QUERY -> paramValues.put(PROP_EXPLODE, Boolean.TRUE);
default -> paramValues.put(PROP_EXPLODE, Boolean.FALSE);
}
}
}
}
private void readApiResponses(MethodElement element, VisitorContext context, Operation swaggerOperation, @Nullable ClassElement jsonViewClass) {
var methodResponseAnns = element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.responses.ApiResponse.class);
processResponses(swaggerOperation, methodResponseAnns, element, context, jsonViewClass);
var classResponseAnns = element.getDeclaringType().getAnnotationValuesByType(io.swagger.v3.oas.annotations.responses.ApiResponse.class);
processResponses(swaggerOperation, classResponseAnns, element, context, jsonViewClass);
}
private void processResponses(Operation operation,
List> responseAnns,
MethodElement element, VisitorContext context, @Nullable ClassElement jsonViewClass) {
ApiResponses apiResponses = operation.getResponses();
if (apiResponses == null) {
apiResponses = new ApiResponses();
operation.setResponses(apiResponses);
}
if (CollectionUtils.isEmpty(responseAnns)) {
return;
}
for (var responseAnn : responseAnns) {
String responseCode = responseAnn.stringValue(PROP_RESPONSE_CODE).orElse("default");
if (apiResponses.containsKey(responseCode)) {
continue;
}
ApiResponse newApiResponse = toValue(responseAnn.getValues(), context, ApiResponse.class, jsonViewClass);
if (newApiResponse != null) {
if (responseAnn.booleanValue("useReturnTypeSchema").orElse(false) && element != null) {
addResponseContent(element, context, Utils.resolveOpenApi(context), newApiResponse, jsonViewClass);
} else {
List producesMediaTypes = producesMediaTypes(element);
var contentAnns = responseAnn.get(PROP_CONTENT, io.swagger.v3.oas.annotations.media.Content[].class).orElse(null);
var mediaTypes = new ArrayList();
if (ArrayUtils.isNotEmpty(contentAnns)) {
for (io.swagger.v3.oas.annotations.media.Content contentAnn : contentAnns) {
if (StringUtils.isNotEmpty(contentAnn.mediaType())) {
mediaTypes.add(contentAnn.mediaType());
} else {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
}
}
Content newContent = newApiResponse.getContent();
if (newContent != null) {
io.swagger.v3.oas.models.media.MediaType defaultMediaType = newContent.get(MediaType.APPLICATION_JSON);
var contentFromProduces = new Content();
for (String mt : mediaTypes) {
if (mt.equals(MediaType.APPLICATION_JSON)) {
for (MediaType mediaType : producesMediaTypes) {
contentFromProduces.put(mediaType.toString(), defaultMediaType);
}
} else {
contentFromProduces.put(mt, newContent.get(mt));
}
}
newApiResponse.setContent(contentFromProduces);
}
}
try {
if (StringUtils.isEmpty(newApiResponse.getDescription())) {
newApiResponse.setDescription(responseCode.equals(PROP_DEFAULT) ? "OK response" : HttpStatus.getDefaultReason(Integer.parseInt(responseCode)));
}
} catch (Exception e) {
newApiResponse.setDescription("Response " + responseCode);
}
apiResponses.put(responseCode, newApiResponse);
}
}
operation.setResponses(apiResponses);
}
// boolean - is swagger schema has implementation
private Pair readSwaggerRequestBody(Element element, List consumesMediaTypes, VisitorContext context) {
var requestBodyAnn = element.findAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class).orElse(null);
if (requestBodyAnn == null) {
return null;
}
boolean hasSchemaImplementation = false;
var contentAnn = requestBodyAnn.getAnnotation(PROP_CONTENT, io.swagger.v3.oas.annotations.media.Content.class).orElse(null);
if (contentAnn != null) {
var schemaAnn = contentAnn.getAnnotation(PROP_SCHEMA, io.swagger.v3.oas.annotations.media.Schema.class).orElse(null);
if (schemaAnn != null) {
hasSchemaImplementation = schemaAnn.stringValue(PROP_IMPLEMENTATION).orElse(null) != null;
}
}
var jsonViewClass = element instanceof ParameterElement ? getJsonViewClass(element, context) : null;
RequestBody requestBody = toValue(requestBodyAnn.getValues(), context, RequestBody.class, jsonViewClass);
// if media type doesn't set in swagger annotation, check micronaut annotation
if (contentAnn != null
&& contentAnn.stringValue(PROP_MEDIA_TYPE).isEmpty()
&& requestBody != null
&& requestBody.getContent() != null
&& !consumesMediaTypes.equals(DEFAULT_MEDIA_TYPES)) {
io.swagger.v3.oas.models.media.MediaType defaultSwaggerMediaType = requestBody.getContent().remove(MediaType.APPLICATION_JSON);
for (MediaType mediaType : consumesMediaTypes) {
requestBody.getContent().put(mediaType.toString(), defaultSwaggerMediaType);
}
}
return Pair.of(requestBody, hasSchemaImplementation);
}
private void readServers(MethodElement element, VisitorContext context, Operation swaggerOperation) {
for (Server server : methodServers(element, context)) {
swaggerOperation.addServersItem(server);
}
}
private void readCallbacks(MethodElement element, VisitorContext context,
Operation swaggerOperation, @Nullable ClassElement jsonViewClass) {
var callbacksAnn = element.getAnnotation(Callbacks.class);
List> callbackAnns;
if (callbacksAnn != null) {
callbackAnns = callbacksAnn.getAnnotations(PROP_VALUE);
} else {
callbackAnns = element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.callbacks.Callback.class);
}
if (CollectionUtils.isEmpty(callbackAnns)) {
return;
}
for (var callbackAnn : callbackAnns) {
String callbackName = callbackAnn.stringValue(PROP_NAME).orElse(null);
if (StringUtils.isEmpty(callbackName)) {
continue;
}
String ref = callbackAnn.stringValue(PROP_REF).orElse(null);
if (StringUtils.isNotEmpty(ref)) {
String refCallback = ref.substring(COMPONENTS_CALLBACKS_PREFIX.length());
processCallbackReference(context, swaggerOperation, callbackName, refCallback);
continue;
}
String expr = callbackAnn.stringValue(PROP_CALLBACK_URL_EXPRESSION).orElse(null);
if (StringUtils.isNotEmpty(expr)) {
processUrlCallbackExpression(context, swaggerOperation, callbackAnn, callbackName, expr, jsonViewClass);
} else {
processCallbackReference(context, swaggerOperation, callbackName, null);
}
}
}
private void processCallbackReference(VisitorContext context, Operation swaggerOperation,
String callbackName, String refCallback) {
Utils.resolveComponents(Utils.resolveOpenApi(context));
Map callbacks = initCallbacks(swaggerOperation);
var callbackRef = new Callback();
callbackRef.set$ref(refCallback != null ? refCallback : COMPONENTS_CALLBACKS_PREFIX + callbackName);
callbacks.put(callbackName, callbackRef);
}
private void processUrlCallbackExpression(VisitorContext context,
Operation operation, AnnotationValue callbackAnn,
String callbackName, final String callbackUrl, @Nullable ClassElement jsonViewClass) {
var operationAnns = callbackAnn.getAnnotations(PROP_OPERATION, io.swagger.v3.oas.annotations.Operation.class);
var pathItem = new PathItem();
if (CollectionUtils.isNotEmpty(operationAnns)) {
for (var operationAnn : operationAnns) {
HttpMethod httpMethod = operationAnn.get(PROP_METHOD, HttpMethod.class).orElse(null);
if (httpMethod == null) {
continue;
}
var op = toValue(operationAnn.getValues(), context, Operation.class, jsonViewClass);
if (op != null) {
setOperationOnPathItem(pathItem, httpMethod, op);
}
}
}
Map callbacks = initCallbacks(operation);
var callback = new Callback();
callback.addPathItem(callbackUrl, pathItem);
callbacks.put(callbackName, callback);
}
private Map initCallbacks(Operation swaggerOperation) {
Map callbacks = swaggerOperation.getCallbacks();
if (callbacks == null) {
callbacks = new LinkedHashMap<>();
swaggerOperation.setCallbacks(callbacks);
}
return callbacks;
}
private void addTagIfNotPresent(String tag, Operation swaggerOperation) {
List tags = swaggerOperation.getTags();
if (tags == null || !tags.contains(tag)) {
swaggerOperation.addTagsItem(tag);
}
}
private void processMicronautVersionAndGroup(Operation swaggerOperation, String url,
HttpMethod httpMethod,
List consumesMediaTypes,
List producesMediaTypes,
MethodElement methodEl, VisitorContext context) {
String methodKey = httpMethod.name()
+ '#' + url
+ '#' + CollectionUtils.toString(CollectionUtils.isEmpty(consumesMediaTypes) ? DEFAULT_MEDIA_TYPES : consumesMediaTypes)
+ '#' + CollectionUtils.toString(CollectionUtils.isEmpty(producesMediaTypes) ? DEFAULT_MEDIA_TYPES : producesMediaTypes);
Map groupPropertiesMap = getGroupsPropertiesMap(context);
var groups = new HashMap();
var excludedGroups = new ArrayList();
ClassElement classEl = methodEl.getDeclaringType();
PackageElement packageEl = classEl.getPackage();
String packageName = packageEl.getName();
processGroups(groups, excludedGroups, methodEl.getAnnotationValuesByType(OpenAPIGroup.class), groupPropertiesMap);
processGroups(groups, excludedGroups, packageEl.getAnnotationValuesByType(OpenAPIGroup.class), groupPropertiesMap);
processGroupsFromIncludedEndpoints(groups, excludedGroups, classEl.getName());
// properties from system properties or from environment more priority than annotations
for (GroupProperties groupProperties : groupPropertiesMap.values()) {
if (CollectionUtils.isNotEmpty(groupProperties.getPackages())) {
for (PackageProperties groupPackage : groupProperties.getPackages()) {
boolean isInclude = groupPackage.isIncludeSubpackages() ? packageName.startsWith(groupPackage.getName()) : packageName.equals(groupPackage.getName());
if (isInclude) {
groups.put(groupProperties.getName(), new EndpointGroupInfo(groupProperties.getName()));
}
}
}
if (CollectionUtils.isNotEmpty(groupProperties.getPackagesExclude())) {
for (PackageProperties excludePackage : groupProperties.getPackagesExclude()) {
boolean isExclude = excludePackage.isIncludeSubpackages() ? packageName.startsWith(excludePackage.getName()) : packageName.equals(excludePackage.getName());
if (isExclude) {
excludedGroups.add(groupProperties.getName());
}
}
}
}
RouterVersioningProperties versioningProperties = getRouterVersioningProperties(context);
boolean isVersioningEnabled = versioningProperties.isEnabled() && versioningProperties.isRouterVersioningEnabled()
&& (versioningProperties.isHeaderEnabled() || versioningProperties.isParameterEnabled());
String version = null;
if (isVersioningEnabled) {
List> versionAnns = methodEl.getAnnotationValuesByType(Version.class);
if (CollectionUtils.isNotEmpty(versionAnns)) {
version = versionAnns.get(0).stringValue().orElse(null);
}
if (version != null) {
Utils.getAllKnownVersions().add(version);
}
if (versioningProperties.isParameterEnabled()) {
addVersionParameters(swaggerOperation, versioningProperties.getParameterNames(), false);
}
if (versioningProperties.isHeaderEnabled()) {
addVersionParameters(swaggerOperation, versioningProperties.getHeaderNames(), true);
}
}
Map> endpointInfosMap = Utils.getEndpointInfos();
if (endpointInfosMap == null) {
endpointInfosMap = new HashMap<>();
Utils.setEndpointInfos(endpointInfosMap);
}
List endpointInfos = endpointInfosMap.computeIfAbsent(methodKey, (k) -> new ArrayList<>());
endpointInfos.add(new EndpointInfo(
url,
httpMethod,
methodEl,
swaggerOperation,
version,
groups,
excludedGroups
));
}
private void processGroups(Map groups,
List excludedGroups,
List> annotationValues,
Map groupPropertiesMap) {
if (CollectionUtils.isEmpty(annotationValues)) {
return;
}
for (AnnotationValue groupAnn : annotationValues) {
excludedGroups.addAll(List.of(groupAnn.stringValues(PROP_EXCLUDE)));
var extensionAnns = groupAnn.getAnnotations(PROP_EXTENSIONS);
for (var groupName : groupAnn.stringValues(PROP_VALUE)) {
var extensions = new HashMap();
if (CollectionUtils.isNotEmpty(extensionAnns)) {
for (Object extensionAnn : extensionAnns) {
processExtensions(extensions, (AnnotationValue) extensionAnn);
}
}
var groupInfo = groups.get(groupName);
if (groupInfo == null) {
groupInfo = new EndpointGroupInfo(groupName);
groups.put(groupName, groupInfo);
}
groupInfo.getExtensions().putAll(extensions);
}
}
Set allKnownGroups = Utils.getAllKnownGroups();
allKnownGroups.addAll(groups.keySet());
allKnownGroups.addAll(excludedGroups);
}
private void processGroupsFromIncludedEndpoints(Map groups, List excludedGroups, String className) {
if (CollectionUtils.isEmpty(Utils.getIncludedClassesGroups()) && CollectionUtils.isEmpty(Utils.getIncludedClassesGroupsExcluded())) {
return;
}
List classGroups = Utils.getIncludedClassesGroups() != null ? Utils.getIncludedClassesGroups().get(className) : Collections.emptyList();
List classExcludedGroups = Utils.getIncludedClassesGroupsExcluded() != null ? Utils.getIncludedClassesGroupsExcluded().get(className) : Collections.emptyList();
for (var classGroup : classGroups) {
if (groups.containsKey(classGroup)) {
continue;
}
groups.put(classGroup, new EndpointGroupInfo(classGroup));
}
excludedGroups.addAll(classExcludedGroups);
Set allKnownGroups = Utils.getAllKnownGroups();
allKnownGroups.addAll(classGroups);
allKnownGroups.addAll(classExcludedGroups);
}
private void addVersionParameters(Operation swaggerOperation, List names, boolean isHeader) {
String in = isHeader ? ParameterIn.HEADER.toString() : ParameterIn.QUERY.toString();
for (String parameterName : names) {
var parameter = new Parameter()
.in(in)
.description("API version")
.name(parameterName)
.schema(setSpecVersion(PrimitiveType.STRING.createProperty()));
swaggerOperation.addParametersItem(parameter);
}
}
private void readTags(MethodElement element, VisitorContext context, Operation swaggerOperation, List classTags, OpenAPI openAPI) {
element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.tags.Tag.class)
.forEach(av -> av.stringValue(PROP_NAME)
.ifPresent(swaggerOperation::addTagsItem));
var copyTags = openAPI.getTags() != null ? new ArrayList<>(openAPI.getTags()) : null;
var operationTags = processOpenApiAnnotation(element, context, io.swagger.v3.oas.annotations.tags.Tag.class, Tag.class, copyTags);
// find not simple tags (tags with description or other information), such fields need to be described at the openAPI level.
List complexTags = null;
if (CollectionUtils.isNotEmpty(operationTags)) {
complexTags = new ArrayList<>();
for (Tag operationTag : operationTags) {
if (StringUtils.hasText(operationTag.getDescription())
|| CollectionUtils.isNotEmpty(operationTag.getExtensions())
|| operationTag.getExternalDocs() != null) {
complexTags.add(operationTag);
}
}
}
if (CollectionUtils.isNotEmpty(complexTags)) {
if (CollectionUtils.isEmpty(openAPI.getTags())) {
openAPI.setTags(complexTags);
} else {
for (Tag complexTag : complexTags) {
// skip all existed tags
boolean alreadyExists = false;
for (Tag apiTag : openAPI.getTags()) {
if (apiTag.getName().equals(complexTag.getName())) {
alreadyExists = true;
break;
}
}
if (!alreadyExists) {
openAPI.getTags().add(complexTag);
}
}
}
}
// only way to get inherited tags
element.getValues(Tags.class, AnnotationValue.class)
.forEach((k, v) -> v.stringValue(PROP_NAME).ifPresent(name -> addTagIfNotPresent((String) name, swaggerOperation)));
classTags.forEach(tag -> addTagIfNotPresent(tag.getName(), swaggerOperation));
if (CollectionUtils.isNotEmpty(swaggerOperation.getTags())) {
swaggerOperation.getTags().sort(Comparator.naturalOrder());
}
}
private List readTags(ClassElement element, VisitorContext context) {
return readTags(element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.tags.Tag.class), context);
}
final List readTags(List> tagAnns, VisitorContext context) {
var tags = new ArrayList();
for (var tagAnn : tagAnns) {
var tag = toValue(tagAnn.getValues(), context, Tag.class, null);
if (tag != null) {
tags.add(tag);
}
}
return tags;
}
private Content buildContent(Element definingElement, ClassElement type, List mediaTypes, OpenAPI openAPI, VisitorContext context, @Nullable ClassElement jsonViewClass) {
var content = new Content();
for (var mediaType : mediaTypes) {
var mt = new io.swagger.v3.oas.models.media.MediaType();
mt.setSchema(resolveSchema(openAPI, definingElement, type, context, Collections.singletonList(mediaType), jsonViewClass, null, null));
content.addMediaType(mediaType.toString(), mt);
}
return content;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy