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

io.micronaut.openapi.visitor.OpenApiApplicationVisitor 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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.micronaut.context.env.Environment;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ElementModifier;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.openapi.OpenApiUtils;
import io.micronaut.openapi.annotation.OpenAPIGroupInfo;
import io.micronaut.openapi.annotation.OpenAPIGroupInfos;
import io.micronaut.openapi.postprocessors.JacksonDiscriminatorPostProcessor;
import io.micronaut.openapi.postprocessors.OpenApiOperationsPostProcessor;
import io.micronaut.openapi.view.OpenApiViewConfig;
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.OpenApiInfo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
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.Paths;
import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
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.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import static io.micronaut.openapi.visitor.ConfigUtils.endpointsConfiguration;
import static io.micronaut.openapi.visitor.ConfigUtils.getAdocProperties;
import static io.micronaut.openapi.visitor.ConfigUtils.getConfigProperty;
import static io.micronaut.openapi.visitor.ConfigUtils.getEnv;
import static io.micronaut.openapi.visitor.ConfigUtils.getExpandableProperties;
import static io.micronaut.openapi.visitor.ConfigUtils.getGroupProperties;
import static io.micronaut.openapi.visitor.ConfigUtils.isOpenApiEnabled;
import static io.micronaut.openapi.visitor.ConfigUtils.isSpecGenerationEnabled;
import static io.micronaut.openapi.visitor.ConfigUtils.readOpenApiConfigFile;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_CLASS_TAGS;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_SECURITY_REQUIREMENTS;
import static io.micronaut.openapi.visitor.ContextProperty.MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_SERVERS;
import static io.micronaut.openapi.visitor.ContextUtils.addGeneratedResource;
import static io.micronaut.openapi.visitor.ContextUtils.info;
import static io.micronaut.openapi.visitor.ContextUtils.warn;
import static io.micronaut.openapi.visitor.FileUtils.EXT_JSON;
import static io.micronaut.openapi.visitor.FileUtils.EXT_YML;
import static io.micronaut.openapi.visitor.FileUtils.calcFinalFilename;
import static io.micronaut.openapi.visitor.FileUtils.getDefaultFilePath;
import static io.micronaut.openapi.visitor.FileUtils.getViewsDestDir;
import static io.micronaut.openapi.visitor.FileUtils.openApiSpecFile;
import static io.micronaut.openapi.visitor.FileUtils.resolve;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.ALL;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_APPLICATION_NAME;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_ADDITIONAL_FILES;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_JSON_FORMAT;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_VIEWS_SPEC;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.SPRING_APPLICATION_NAME;
import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_SECURITY;
import static io.micronaut.openapi.visitor.OpenApiNormalizeUtils.findAndRemoveDuplicates;
import static io.micronaut.openapi.visitor.OpenApiNormalizeUtils.normalizeOpenApi;
import static io.micronaut.openapi.visitor.OpenApiNormalizeUtils.removeEmptyComponents;
import static io.micronaut.openapi.visitor.SchemaDefinitionUtils.toValue;
import static io.micronaut.openapi.visitor.SchemaUtils.copyOpenApi;
import static io.micronaut.openapi.visitor.SchemaUtils.getOperationOnPathItem;
import static io.micronaut.openapi.visitor.SchemaUtils.resolveSchemas;
import static io.micronaut.openapi.visitor.SchemaUtils.setOperationOnPathItem;
import static io.micronaut.openapi.visitor.StringUtil.PLACEHOLDER_POSTFIX;
import static io.micronaut.openapi.visitor.StringUtil.PLACEHOLDER_PREFIX;
import static io.micronaut.openapi.visitor.StringUtil.QUOTE;
import static io.micronaut.openapi.visitor.Utils.resolveComponents;
import static io.swagger.v3.oas.models.Components.COMPONENTS_SCHEMAS_REF;

/**
 * Visits the application class.
 *
 * @author graemerocher
 * @since 1.0
 */
public class OpenApiApplicationVisitor extends AbstractOpenApiVisitor implements TypeElementVisitor {

    public static final String DEFAULT_OPENAPI_TITLE = "Service";
    public static final String DEFAULT_OPENAPI_VERSION = "1.0.0";

    private static final int MAX_ITERATIONS = 100;
    private ClassElement classElement;
    private int visitedElements = -1;

    @Override
    public void start(VisitorContext context) {
        Utils.init(context);
    }

    @Override
    public Set getSupportedOptions() {
        return ALL;
    }

    @Override
    public void visitClass(ClassElement element, VisitorContext context) {
        try {
            if (!isOpenApiEnabled(context) || !isSpecGenerationEnabled(context)) {
                return;
            }
            if (ignore(element, context)) {
                return;
            }
            incrementVisitedElements(context);

            info("Generating OpenAPI Documentation", context);
            OpenAPI openApi = readOpenApi(element, context);

            // Handle Application securityRequirements schemes
            processSecuritySchemes(element, context);

            mergeAdditionalSwaggerFiles(element, context, openApi);
            // handle type level tags
            List tagList = processOpenApiAnnotation(
                element,
                context,
                io.swagger.v3.oas.annotations.tags.Tag.class,
                Tag.class,
                openApi.getTags()
            );
            openApi.setTags(tagList);

            // handle type level security requirements
            List securityRequirements = readSecurityRequirements(element);
            if (openApi.getSecurity() != null) {
                securityRequirements.addAll(openApi.getSecurity());
            }

            openApi.setSecurity(securityRequirements);

            // handle type level servers
            List servers = processOpenApiAnnotation(
                element,
                context,
                io.swagger.v3.oas.annotations.servers.Server.class,
                Server.class,
                openApi.getServers()
            );
            openApi.setServers(servers);

            OpenAPI existing = ContextUtils.get(Utils.ATTR_OPENAPI, OpenAPI.class, context);
            if (existing != null) {
                if (openApi.getInfo() != null) {
                    existing.setInfo(openApi.getInfo());
                }
                copyOpenApi(existing, openApi);
            } else {
                ContextUtils.put(Utils.ATTR_OPENAPI, openApi, context);
            }

            if (Utils.isTestMode()) {
                Utils.resolveOpenApi(context);
            }

            classElement = element;
        } catch (Exception e) {
            warn("Error with processing class:\n" + Utils.printStackTrace(e), context, classElement);
        }
    }

    private boolean ignore(ClassElement element, VisitorContext context) {

        return !element.isAnnotationPresent(OpenAPIDefinition.class)
            && !element.isAnnotationPresent(OpenAPIGroupInfo.class)
            && !element.isAnnotationPresent(OpenAPIGroupInfos.class);
    }

    /**
     * Merge the OpenAPI YAML and JSON files into one single file.
     *
     * @param element The element
     * @param context The visitor context
     * @param openApi The {@link OpenAPI} object for the application
     */
    private void mergeAdditionalSwaggerFiles(ClassElement element, VisitorContext context, OpenAPI openApi) {
        String additionalSwaggerFiles = getConfigProperty(MICRONAUT_OPENAPI_ADDITIONAL_FILES, context);
        if (StringUtils.isEmpty(additionalSwaggerFiles)) {
            return;
        }
        Path directory = resolve(context, java.nio.file.Paths.get(additionalSwaggerFiles));
        if (!Files.isDirectory(directory)) {
            warn(directory + " does not exist or is not a directory", context, element);
            return;
        }
        info("Merging Swagger OpenAPI YAML and JSON files from location: " + directory, context);
        try (DirectoryStream paths = Files.newDirectoryStream(directory, path -> {
            var pathStr = path.toString().toLowerCase();
            return FileUtils.isYaml(pathStr) || FileUtils.isJson(pathStr);
        })) {
            for (var path : paths) {
                boolean isYaml = FileUtils.isYaml(path.toString().toLowerCase());
                info("Reading Swagger OpenAPI " + (isYaml ? "YAML" : "JSON") + " file " + path.getFileName(), context);
                OpenAPI parsedOpenApi = null;
                try {
                    parsedOpenApi = (isYaml ? Utils.getYamlMapper() : Utils.getJsonMapper()).readValue(path.toFile(), OpenAPI.class);
                } catch (IOException e) {
                    warn("Unable to read file " + path.getFileName() + ": " + e.getMessage(), context, element);
                }
                copyOpenApi(openApi, parsedOpenApi);
            }
        } catch (IOException e) {
            warn("Unable to read  file from " + directory + ": " + e.getMessage(), context, element);
        }
    }

    private OpenAPI readOpenApi(ClassElement element, VisitorContext context) {
        var openApiDefAnn = element.findAnnotation(OpenAPIDefinition.class).orElse(null);
        if (openApiDefAnn == null) {
            var openApi = new OpenAPI();
            if (Utils.isOpenapi31()) {
                openApi.openapi(OpenApiUtils.OPENAPI_31_VERSION)
                    .jsonSchemaDialect(ConfigUtils.getJsonSchemaDialect(context))
                    .specVersion(SpecVersion.V31);
            }
            return openApi;
        }

        var openApi = toValue(openApiDefAnn.getValues(), context, OpenAPI.class, null);
        if (openApi == null) {
            openApi = new OpenAPI();
        }
        if (Utils.isOpenapi31()) {
            openApi.openapi(OpenApiUtils.OPENAPI_31_VERSION)
                .jsonSchemaDialect(ConfigUtils.getJsonSchemaDialect(context))
                .specVersion(SpecVersion.V31);
        }
        var secRequirementAnns = openApiDefAnn.getAnnotations(PROP_SECURITY, io.swagger.v3.oas.annotations.security.SecurityRequirement.class);
        if (CollectionUtils.isNotEmpty(secRequirementAnns)) {
            var securityRequirements = new ArrayList();
            for (var secRequirementAnn : secRequirementAnns) {
                securityRequirements.add(ConvertUtils.mapToSecurityRequirement(secRequirementAnn));
            }
            openApi.setSecurity(securityRequirements);
        }
        return openApi;
    }

    private void renderViews(String title, Map, OpenApiInfo> openApiInfos, Path destinationDir, VisitorContext context) throws IOException {
        String viewSpecification = getConfigProperty(MICRONAUT_OPENAPI_VIEWS_SPEC, context);
        OpenApiViewConfig cfg = OpenApiViewConfig.fromSpecification(viewSpecification, openApiInfos, readOpenApiConfigFile(context), context);
        if (cfg.isEnabled()) {
            cfg.setTitle(title);
            if (CollectionUtils.isNotEmpty(openApiInfos)) {
                cfg.setSpecFile(openApiInfos.values().iterator().next().getSpecFilePath());
            }
            cfg.render(destinationDir, context);
        }
    }

    private static PropertyNamingStrategies.NamingBase fromName(String name) {
        if (name == null) {
            return null;
        }
        var strategy = switch (name.toUpperCase(Locale.ENGLISH)) {
            case "LOWER_CAMEL_CASE" -> PropertyNamingStrategies.LOWER_CAMEL_CASE;
            case "UPPER_CAMEL_CASE" -> PropertyNamingStrategies.UPPER_CAMEL_CASE;
            case "SNAKE_CASE" -> PropertyNamingStrategies.SNAKE_CASE;
            case "UPPER_SNAKE_CASE" -> PropertyNamingStrategies.UPPER_SNAKE_CASE;
            case "LOWER_CASE" -> PropertyNamingStrategies.LOWER_CASE;
            case "KEBAB_CASE" -> PropertyNamingStrategies.KEBAB_CASE;
            case "LOWER_DOT_CASE" -> PropertyNamingStrategies.LOWER_DOT_CASE;
            default -> null;
        };
        return (PropertyNamingStrategies.NamingBase) strategy;
    }

    private void applyPropertyNamingStrategy(OpenAPI openAPI, VisitorContext context) {
        var namingStrategyName = getConfigProperty(MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY, context);
        var propertyNamingStrategy = fromName(namingStrategyName);
        if (propertyNamingStrategy == null) {
            return;
        }
        info("Using " + namingStrategyName + " property naming strategy.", context);
        if (openAPI.getComponents() == null || CollectionUtils.isEmpty(openAPI.getComponents().getSchemas())) {
            return;
        }
        for (var schema : openAPI.getComponents().getSchemas().values()) {
            Map properties = schema.getProperties();
            if (properties != null) {
                var newProps = new LinkedHashMap(properties.size());
                for (var entry : properties.entrySet()) {
                    newProps.put(propertyNamingStrategy.translate(entry.getKey()), entry.getValue());
                }
                schema.setProperties(newProps);
            }
            List required = schema.getRequired();
            if (CollectionUtils.isNotEmpty(required)) {
                var newRequired = new ArrayList(required.size());
                for (var req : required) {
                    newRequired.add(propertyNamingStrategy.translate(req));
                }
                schema.setRequired(newRequired);
            }
        }
    }

    private void applyPropertyServerContextPath(OpenAPI openAPI, VisitorContext context) {
        final String serverContextPath = getConfigProperty(MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH, context);
        if (StringUtils.isEmpty(serverContextPath)) {
            return;
        }
        info("Applying server context path: " + serverContextPath + " to Paths.", context);
        Paths paths = openAPI.getPaths();
        if (CollectionUtils.isEmpty(paths)) {
            return;
        }
        var newPaths = new Paths();
        for (Map.Entry path : paths.entrySet()) {
            final String mapping = path.getKey();
            String newPath = mapping.startsWith(serverContextPath) ? mapping : StringUtils.prependUri(serverContextPath, mapping);
            if (!newPath.startsWith(StringUtil.SLASH) && !newPath.startsWith(StringUtil.DOLLAR)) {
                newPath = StringUtil.SLASH + newPath;
            }
            newPaths.addPathItem(newPath, path.getValue());
        }
        openAPI.setPaths(newPaths);
    }

    public static JsonNode resolvePlaceholders(ArrayNode anode, UnaryOperator propertyExpander) {
        for (int i = 0; i < anode.size(); ++i) {
            anode.set(i, resolvePlaceholders(anode.get(i), propertyExpander));
        }
        return anode;
    }

    public static JsonNode resolvePlaceholders(ObjectNode onode, UnaryOperator propertyExpander) {
        if (onode.isEmpty()) {
            return onode;
        }
        final ObjectNode newNode = onode.objectNode();
        var i = onode.fields();
        while (i.hasNext()) {
            var entry = i.next();
            newNode.set(propertyExpander.apply(entry.getKey()), resolvePlaceholders(entry.getValue(), propertyExpander));
        }
        return newNode;
    }

    public static JsonNode resolvePlaceholders(JsonNode node, UnaryOperator propertyExpander) {
        if (node.isTextual()) {
            final String text = node.textValue();
            if (text == null || text.isBlank()) {
                return node;
            }
            final String newText = propertyExpander.apply(text);
            return text.equals(newText) ? node : TextNode.valueOf(newText);
        } else if (node.isArray()) {
            return resolvePlaceholders((ArrayNode) node, propertyExpander);
        } else if (node.isObject()) {
            return resolvePlaceholders((ObjectNode) node, propertyExpander);
        } else {
            return node;
        }
    }

    public static String expandProperties(String s, List> properties, VisitorContext context) {
        if (StringUtils.isEmpty(s) || !s.contains(PLACEHOLDER_PREFIX)) {
            return s;
        }

        // form openapi file (expandable properties)
        if (CollectionUtils.isNotEmpty(properties)) {
            for (Pair entry : properties) {
                s = s.replaceAll(entry.getFirst(), entry.getSecond());
            }
        }

        return replacePlaceholders(s, context);
    }

    public static String replacePlaceholders(String value, VisitorContext context) {
        if (StringUtils.isEmpty(value) || !value.contains(PLACEHOLDER_PREFIX)) {
            return value;
        }
        // system properties
        if (CollectionUtils.isNotEmpty(System.getProperties())) {
            for (Map.Entry sysProp : System.getProperties().entrySet()) {
                value = value.replace(PLACEHOLDER_PREFIX + sysProp.getKey().toString() + PLACEHOLDER_POSTFIX, sysProp.getValue().toString());
            }
        }

        // form openapi file
        for (Map.Entry fileProp : readOpenApiConfigFile(context).entrySet()) {
            value = value.replace(PLACEHOLDER_PREFIX + fileProp.getKey().toString() + PLACEHOLDER_POSTFIX, fileProp.getValue().toString());
        }

        // from environments
        Environment environment = getEnv(context);
        if (environment != null) {
            value = environment.getPlaceholderResolver().resolvePlaceholders(value).orElse(value);
        }

        return value;
    }

    private static OpenAPI resolvePropertyPlaceHolders(OpenAPI openAPI, VisitorContext context) {
        List> expandableProperties = getExpandableProperties(context);
        if (CollectionUtils.isNotEmpty(expandableProperties)) {
            info("Expanding properties: " + expandableProperties, context);
        }
        JsonNode root = resolvePlaceholders(Utils.getYamlMapper().convertValue(openAPI, ObjectNode.class), s -> expandProperties(s, expandableProperties, context));
        return Utils.getYamlMapper().convertValue(root, OpenAPI.class);
    }

    @Override
    public void finish(VisitorContext context) {
        try {
            if (!isOpenApiEnabled(context)) {
                return;
            }
            if (visitedElements == visitedElements(context)) {
                // nothing new visited, avoid rewriting the files.
                return;
            }

            Map, OpenApiInfo> openApiInfos = null;
            String documentTitle = "OpenAPI";

            if (isSpecGenerationEnabled(context)) {
                OpenAPI openApi = ContextUtils.get(Utils.ATTR_OPENAPI, OpenAPI.class, context);
                if (openApi == null) {
                    return;
                }
                processEndpoints(context);

                mergeMicronautEndpointInfos(openApi, context);
                openApiInfos = divideOpenapiByGroupsAndVersions(openApi, context);
                if (Utils.isTestMode()) {
                    Utils.setTestReferences(openApiInfos);
                }

                String isJson = getConfigProperty(MICRONAUT_OPENAPI_JSON_FORMAT, context);
                boolean isYaml = !(StringUtils.isNotEmpty(isJson) && isJson.equalsIgnoreCase(StringUtils.TRUE));
                String ext = isYaml ? EXT_YML : EXT_JSON;

                for (Map.Entry, OpenApiInfo> entry : openApiInfos.entrySet()) {

                    OpenApiInfo openApiInfo = entry.getValue();

                    openApi = openApiInfo.getOpenApi();

                    openApi = postProcessOpenApi(openApi, context);
                    openApiInfo.setOpenApi(openApi);
                    // need to set test reference to openApi after post-processing
                    if (Utils.isTestMode()) {
                        Utils.setTestReference(openApi);
                    }

                    var titleAndFilename = calcFinalFilename(openApiInfo.getFilename(), openApiInfo, openApiInfos.size() == 1, ext, context);
                    documentTitle = titleAndFilename.getFirst();
                    openApiInfo.setFilename(titleAndFilename.getSecond());
                }

                writeYamlToFile(openApiInfos, documentTitle, context, isYaml);
            }

            generateViews(documentTitle, openApiInfos, context);

            visitedElements = visitedElements(context);
        } catch (Exception e) {
            warn("Error:\n" + Utils.printStackTrace(e), context);
            throw e;
        }
    }

    private Map, OpenApiInfo> divideOpenapiByGroupsAndVersions(OpenAPI openApi, VisitorContext context) {
        Map> endpointInfosMap = Utils.getEndpointInfos();
        Set allVersions = Utils.getAllKnownVersions();
        Set allGroups = Utils.getAllKnownGroups();
        if (CollectionUtils.isEmpty(endpointInfosMap)
            || (CollectionUtils.isEmpty(allVersions) && CollectionUtils.isEmpty(allGroups))) {
            return Collections.singletonMap(Pair.NULL_STRING_PAIR, new OpenApiInfo(openApi));
        }

        var commonEndpoints = new ArrayList();

        // key version, groupName
        var result = new HashMap, OpenApiInfo>();

        for (List endpointInfos : endpointInfosMap.values()) {
            for (EndpointInfo endpointInfo : endpointInfos) {
                if (CollectionUtils.isEmpty(endpointInfo.getGroups()) && endpointInfo.getVersion() == null) {
                    commonEndpoints.add(endpointInfo);
                    continue;
                }
                for (EndpointGroupInfo endpointGroupInfo : endpointInfo.getGroups().values()) {
                    if (CollectionUtils.isNotEmpty(endpointInfo.getExcludedGroups())
                        && endpointInfo.getExcludedGroups().contains(endpointGroupInfo.getName())) {
                        continue;
                    }
                    OpenAPI newOpenApi = addOpenApiInfo(endpointGroupInfo.getName(), endpointInfo.getVersion(), openApi, result, context);
                    addOperation(endpointInfo, newOpenApi, endpointGroupInfo, context);
                }

                // if we have only versions without groups
                if (CollectionUtils.isEmpty(endpointInfo.getGroups())) {
                    OpenAPI newOpenApi = addOpenApiInfo(null, endpointInfo.getVersion(), openApi, result, context);
                    addOperation(endpointInfo, newOpenApi, null, context);
                }
            }
        }

        // add common endpoints (without group name)
        for (Map.Entry, OpenApiInfo> entry : result.entrySet()) {

            String group = entry.getKey().getFirst();
            GroupProperties groupProperties = getGroupProperties(group, context);
            if (groupProperties != null && groupProperties.getCommonExclude() != null && groupProperties.getCommonExclude()) {
                continue;
            }

            OpenAPI groupOpenApi = entry.getValue().getOpenApi();

            for (EndpointInfo commonEndpoint : commonEndpoints) {
                if (CollectionUtils.isNotEmpty(commonEndpoint.getExcludedGroups()) && commonEndpoint.getExcludedGroups().contains(group)) {
                    continue;
                }
                addOperation(commonEndpoint, groupOpenApi, null, context);
            }
        }

        return result;
    }

    private void addOperation(EndpointInfo endpointInfo, OpenAPI openApi, @Nullable EndpointGroupInfo endpointGroupInfo, VisitorContext context) {
        if (openApi == null) {
            return;
        }
        Paths paths = openApi.getPaths();
        if (paths == null) {
            paths = new Paths();
            openApi.setPaths(paths);
        }
        PathItem pathItem = paths.computeIfAbsent(endpointInfo.getUrl(), (pathUrl) -> new PathItem());
        Operation operation = getOperationOnPathItem(pathItem, endpointInfo.getHttpMethod());
        if (operation == null) {
            Operation opCopy = null;
            try {
                opCopy = OpenApiUtils.getJsonMapper().treeToValue(OpenApiUtils.getJsonMapper().valueToTree(endpointInfo.getOperation()), Operation.class);
                if (endpointGroupInfo != null) {
                    addExtensions(opCopy, endpointGroupInfo.getExtensions());
                }
            } catch (JsonProcessingException e) {
                warn("Error\n" + Utils.printStackTrace(e), context);
            }
            setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), opCopy != null ? opCopy : endpointInfo.getOperation());
            return;
        }
        var mergedOp = SchemaUtils.mergeOperations(operation, endpointInfo.getOperation());
        if (endpointGroupInfo != null) {
            addExtensions(mergedOp, endpointGroupInfo.getExtensions());
        }
        setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), mergedOp);
    }

    private void addExtensions(Operation operation, Map extensions) {
        if (CollectionUtils.isEmpty(extensions)) {
            return;
        }
        for (var ext : extensions.entrySet()) {
            operation.addExtension(ext.getKey().toString(), ext.getValue());
        }
    }

    private OpenAPI addOpenApiInfo(String groupName, String version, OpenAPI openApi,
                                   Map, OpenApiInfo> openApiInfoMap,
                                   VisitorContext context) {
        GroupProperties groupProperties = getGroupProperties(groupName, context);
        boolean hasGroupProperties = groupProperties != null;

        var key = Pair.of(groupName, version);
        OpenApiInfo openApiInfo = openApiInfoMap.get(key);
        OpenAPI newOpenApi;
        if (openApiInfo == null) {

            Map knownOpenApis = Utils.getOpenApis();
            if (CollectionUtils.isNotEmpty(knownOpenApis) && knownOpenApis.containsKey(groupName)) {
                newOpenApi = knownOpenApis.get(groupName);
            } else {
                newOpenApi = new OpenAPI();
            }

            openApiInfo = new OpenApiInfo(
                version,
                groupName,
                hasGroupProperties ? groupProperties.getDisplayName() : null,
                hasGroupProperties ? groupProperties.getFilename() : null,
                !hasGroupProperties || groupProperties.getAdocEnabled() == null || groupProperties.getAdocEnabled(),
                hasGroupProperties ? groupProperties.getAdocFilename() : null,
                newOpenApi
            );

            openApiInfoMap.put(key, openApiInfo);

            OpenAPI openApiCopy;
            try {
                openApiCopy = Utils.getJsonMapper().treeToValue(Utils.getJsonMapper().valueToTree(openApi), OpenAPI.class);
            } catch (JsonProcessingException e) {
                warn("Error\n" + Utils.printStackTrace(e), context);
                return null;
            }

            if (CollectionUtils.isEmpty(knownOpenApis) || !knownOpenApis.containsKey(groupName)) {
                newOpenApi.setTags(openApiCopy.getTags());
                newOpenApi.setServers(openApiCopy.getServers());
                newOpenApi.setInfo(openApiCopy.getInfo());
                newOpenApi.setSecurity(openApiCopy.getSecurity());
                newOpenApi.setExternalDocs(openApiCopy.getExternalDocs());
                newOpenApi.setExtensions(openApiCopy.getExtensions());
            }

            // if we have SecuritySchemes specified only for group
            var groupSecuritySchemes = newOpenApi.getComponents() != null ? newOpenApi.getComponents().getSecuritySchemes() : null;
            if (CollectionUtils.isNotEmpty(groupSecuritySchemes)
                && openApiCopy.getComponents() != null
                && CollectionUtils.isNotEmpty(openApiCopy.getComponents().getSecuritySchemes())) {

                for (var entry : openApiCopy.getComponents().getSecuritySchemes().entrySet()) {
                    if (!groupSecuritySchemes.containsKey(entry.getKey())) {
                        groupSecuritySchemes.put(entry.getKey(), entry.getValue());
                    }
                }
            }

            newOpenApi.setComponents(openApiCopy.getComponents());
            if (CollectionUtils.isNotEmpty(groupSecuritySchemes)) {
                resolveComponents(newOpenApi).setSecuritySchemes(groupSecuritySchemes);
            }
        } else {
            newOpenApi = openApiInfo.getOpenApi();
        }

        return newOpenApi;
    }

    private void mergeMicronautEndpointInfos(OpenAPI openApi, VisitorContext context) {

        Map> endpointInfosMap = Utils.getEndpointInfos();
        if (CollectionUtils.isEmpty(endpointInfosMap)) {
            return;
        }
        // we need to merge operations for single path without versions
        for (List endpointInfos : endpointInfosMap.values()) {
            for (EndpointInfo endpointInfo : endpointInfos) {
                PathItem pathItem = openApi.getPaths().get(endpointInfo.getUrl());
                Operation operation = getOperationOnPathItem(pathItem, endpointInfo.getHttpMethod());
                if (operation == null) {
                    setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), endpointInfo.getOperation());
                    continue;
                }
                if (endpointInfo.getVersion() == null && CollectionUtils.isEmpty(endpointInfo.getGroups())) {
                    setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), SchemaUtils.mergeOperations(operation, endpointInfo.getOperation()));
                }
            }
        }
    }

    @Override
    public int getOrder() {
        return 100;
    }

    private OpenAPI postProcessOpenApi(OpenAPI openApi, VisitorContext context) {

        fixInfoBlockIfNeeded(openApi, context);
        applyPropertyNamingStrategy(openApi, context);
        applyPropertyServerContextPath(openApi, context);

        normalizeOpenApi(openApi, context);
        normalizeOpenApi(openApi, context);
        // Process after sorting so order is stable
        new JacksonDiscriminatorPostProcessor().addMissingDiscriminatorType(openApi);
        new OpenApiOperationsPostProcessor().processOperations(openApi);

        removeUnusedSchemas(openApi);

        removeEmptyComponents(openApi);
        findAndRemoveDuplicates(openApi);

        addExtraSchemas(openApi, context);

        openApi = resolvePropertyPlaceHolders(openApi, context);

        return openApi;
    }

    private void fixInfoBlockIfNeeded(OpenAPI openApi, VisitorContext context) {

        if (openApi.getInfo() == null) {
            openApi.setInfo(new Info());
        }
        var info = openApi.getInfo();
        if (info.getTitle() == null) {
            String applicationName = ConfigUtils.getConfigProperty(MICRONAUT_APPLICATION_NAME, context);
            if (applicationName == null) {
                applicationName = ConfigUtils.getConfigProperty(SPRING_APPLICATION_NAME, context);
            }
            info.setTitle(applicationName != null ? applicationName : DEFAULT_OPENAPI_TITLE);
        }
        if (info.getVersion() == null) {
            info.setVersion(DEFAULT_OPENAPI_VERSION);
        }
    }

    public static void removeUnusedSchemas(OpenAPI openApi) {
        int i = 0;
        // remove unused schemas
        while (removeUnusedSchemasIter(openApi) && i < MAX_ITERATIONS) {
            i++;
        }
    }

    public static boolean removeUnusedSchemasIter(OpenAPI openApi) {
        if (openApi.getComponents() == null) {
            return false;
        }
        Map schemas = openApi.getComponents().getSchemas();
        if (CollectionUtils.isEmpty(schemas)) {
            return false;
        }

        var extraSchemas = OpenApiExtraSchemaVisitor.getExtraSchemas();
        var removed = false;

        try {
            String openApiJson = Utils.getJsonMapper().writeValueAsString(openApi);
            // Create a copy of the keySet so that we can modify the map while in a foreach
            var keySet = new HashSet<>(schemas.keySet());
            for (String schemaName : keySet) {
                if (!openApiJson.contains(QUOTE + COMPONENTS_SCHEMAS_REF + schemaName + QUOTE)
                    && !extraSchemas.containsKey(schemaName)
                ) {
                    schemas.remove(schemaName);
                    removed = true;
                }
            }
            // check excluded extra schemas also
            for (String schemaName : OpenApiExtraSchemaVisitor.getExcludedExtraSchemas()) {
                schemas.remove(schemaName);
            }
        } catch (JsonProcessingException e) {
            // do nothing
        }
        return removed;
    }

    private void addExtraSchemas(OpenAPI openApi, VisitorContext context) {
        var extraSchemas = OpenApiExtraSchemaVisitor.getExtraSchemas();
        if (CollectionUtils.isEmpty(extraSchemas)) {
            return;
        }
        var schemas = resolveSchemas(openApi);
        for (var entry : extraSchemas.entrySet()) {
            if (schemas.containsKey(entry.getKey())) {
                continue;
            }
            schemas.put(entry.getKey(), entry.getValue());
        }
    }

    private void generateViews(@Nullable String documentTitle, @Nullable Map, OpenApiInfo> openApiInfos, VisitorContext context) {
        Path viewsDestDirs = getViewsDestDir(getDefaultFilePath("dummy" + System.nanoTime(), context), context);
        if (viewsDestDirs == null) {
            return;
        }
        String viewSpecification = getConfigProperty(MICRONAUT_OPENAPI_VIEWS_SPEC, context);
        OpenApiViewConfig cfg = OpenApiViewConfig.fromSpecification(viewSpecification, openApiInfos, readOpenApiConfigFile(context), context);
        if (!cfg.isEnabled()) {
            return;
        }
        if (context != null) {
            info("Writing OpenAPI views to destination: " + viewsDestDirs, context);
            var classesOutputPath = ContextUtils.getClassesOutputPath(context);
            if (classesOutputPath != null) {
                addGeneratedResource(classesOutputPath.relativize(viewsDestDirs).toString(), context);
                addGeneratedResource(classesOutputPath.relativize(viewsDestDirs.getParent()).toString(), context);
            }
        }
        try {
            renderViews(documentTitle, openApiInfos, viewsDestDirs, context);
        } catch (Exception e) {

            String swaggerFiles = StringUtils.EMPTY_STRING;
            if (openApiInfos != null) {
                swaggerFiles = openApiInfos.values().stream()
                    .map(OpenApiInfo::getSpecFilePath)
                    .collect(Collectors.joining(", ", "files ", StringUtils.EMPTY_STRING));
            }

            warn("Unable to render swagger view: " + swaggerFiles + " - " + e.getMessage() + ".\n" + Utils.printStackTrace(e), context, classElement);
        }
    }

    private void writeYamlToFile(Map, OpenApiInfo> openApiInfos, String documentTitle, VisitorContext context, boolean isYaml) {

        var isAdocModuleInClassPath = false;
        var isGlobalAdocEnabled = ConfigUtils.isAdocEnabled(context);

        try {
            Class.forName("io.micronaut.openapi.adoc.OpenApiToAdocConverter");
            isAdocModuleInClassPath = true;
        } catch (ClassNotFoundException e) {
            // do nothing
        }

        var objectMapper = isYaml ? Utils.getYamlMapper() : Utils.getJsonMapper();

        for (OpenApiInfo openApiInfo : openApiInfos.values()) {
            Path specFile = openApiSpecFile(openApiInfo.getFilename(), context);
            try (Writer writer = getFileWriter(specFile)) {
                objectMapper.writeValue(writer, openApiInfo.getOpenApi());
                if (Utils.isTestMode()) {
                    Utils.setTestFileName(openApiInfo.getFilename());
                    if (isYaml) {
                        Utils.setTestYamlReference(writer.toString());
                    } else {
                        Utils.setTestJsonReference(writer.toString());
                    }
                } else {
                    info("Writing OpenAPI file to destination: " + specFile, context);
                    var classesOutputPath = ContextUtils.getClassesOutputPath(context);
                    if (classesOutputPath != null) {
                        // add relative paths for the specFile, and its parent META-INF/swagger
                        // so that micronaut-graal visitor knows about them
                        addGeneratedResource(classesOutputPath.relativize(specFile).toString(), context);
                        addGeneratedResource(classesOutputPath.relativize(specFile.getParent()).toString(), context);
                    }
                    openApiInfo.setSpecFilePath(specFile.getFileName().toString());
                }
                if (isAdocModuleInClassPath && isGlobalAdocEnabled && openApiInfo.isAdocEnabled()) {
                    var adocProperties = getAdocProperties(openApiInfo, openApiInfos.size() == 1, context);
                    AdocModule.convert(openApiInfo, adocProperties, context);
                }
            } catch (Exception e) {
                warn("Unable to generate swagger" + (isYaml ? EXT_YML : EXT_JSON) + ": " + specFile + " - " + e.getMessage() + ".\n" + Utils.printStackTrace(e), context, classElement);
            }
        }
    }

    private Writer getFileWriter(Path specFile) throws IOException {
        if (Utils.isTestMode()) {
            return new StringWriter();
        } else if (specFile != null) {
            return Files.newBufferedWriter(specFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
        } else {
            throw new IOException("Swagger spec file location is not present");
        }
    }

    private void processEndpoints(VisitorContext context) {
        EndpointsConfiguration endpointsCfg = endpointsConfiguration(context);
        if (endpointsCfg.isEnabled() && CollectionUtils.isNotEmpty(endpointsCfg.getEndpoints())) {
            var visitor = new OpenApiEndpointVisitor(true);
            for (var endpoint : endpointsCfg.getEndpoints().values()) {
                ClassElement classEl = endpoint.getClassElement();
                if (classEl == null) {
                    continue;
                }
                ContextUtils.put(MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_CLASS_TAGS, endpoint.getTags(), context);
                ContextUtils.put(MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_SERVERS, endpoint.getServers(), context);
                ContextUtils.put(MICRONAUT_INTERNAL_OPENAPI_ENDPOINT_SECURITY_REQUIREMENTS, endpoint.getSecurityRequirements(), context);
                visitor.visitClass(classEl, context);
                for (MethodElement methodEl : classEl.getEnclosedElements(ElementQuery.ALL_METHODS
                    .modifiers(mods -> !mods.contains(ElementModifier.STATIC) && !mods.contains(ElementModifier.PRIVATE))
                    .named(name -> !name.contains(StringUtil.DOLLAR)))) {
                    visitor.visitMethod(methodEl, context);
                }

            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy