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

io.micronaut.openapi.view.OpenApiViewConfig 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.view;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.io.scan.DefaultClassPathResourceLoader;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.openapi.visitor.ConfigUtils;
import io.micronaut.openapi.visitor.ContextUtils;
import io.micronaut.openapi.visitor.Pair;
import io.micronaut.openapi.visitor.group.OpenApiInfo;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import static io.micronaut.openapi.visitor.ConfigUtils.getProjectPath;
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.readFile;
import static io.micronaut.openapi.visitor.FileUtils.resolve;
import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH;
import static io.micronaut.openapi.visitor.StringUtil.COMMA;
import static io.micronaut.openapi.visitor.StringUtil.DOLLAR;
import static io.micronaut.openapi.visitor.StringUtil.SLASH;

/**
 * OpenApi view configuration for Swagger UI, ReDoc, OpenAPI Explorer and RapiDoc.
 * By default, no views are enabled.
 *
 * @author croudet
 * @see Swagger UI
 * @see ReDoc
 * @see RapiDoc
 * @see OpenAPI Explorer
 */
public final class OpenApiViewConfig {

    public static final String DEFAULT_SPEC_MAPPING_PATH = "swagger";

    public static final String RESOURCE_DIR = "res";
    public static final String THEMES_DIR = "theme";
    public static final String TEMPLATES = "templates";
    public static final String TEMPLATES_RAPIPDF = "rapipdf";
    public static final String TEMPLATES_SWAGGER_UI = "swagger-ui";
    public static final String TEMPLATES_REDOC = "redoc";
    public static final String TEMPLATES_RAPIDOC = "rapidoc";
    public static final String TEMPLATES_OPENAPI_EXPLORER = "openapi-explorer";

    private static final String TEMPLATE_INDEX_HTML = "index.html";
    private static final String REDOC = "redoc";
    private static final String RAPIDOC = "rapidoc";
    private static final String SWAGGER_UI = "swagger-ui";
    private static final String OPENAPI_EXPLORER = "openapi-explorer";
    private static final String TEMPLATE_OAUTH_2_REDIRECT_HTML = "oauth2-redirect.html";

    private String mappingPath;
    private String title;
    private String specFile;
    private SwaggerUIConfig swaggerUIConfig;
    private RedocConfig redocConfig;
    private RapidocConfig rapidocConfig;
    private OpenApiExplorerConfig openApiExplorerConfig;
    private final Map, OpenApiInfo> openApiInfos;

    /**
     * The Renderer types.
     */
    public enum RendererType {

        SWAGGER_UI(TEMPLATES_SWAGGER_UI),
        REDOC(TEMPLATES_REDOC),
        RAPIDOC(TEMPLATES_RAPIDOC),
        OPENAPI_EXPLORER(TEMPLATES_OPENAPI_EXPLORER),
        ;

        private final String templatePath;

        RendererType(String templatePath) {
            this.templatePath = templatePath;
        }

        public String getTemplatePath() {
            return templatePath;
        }
    }

    private OpenApiViewConfig(Map, OpenApiInfo> openApiInfos) {
        this.openApiInfos = openApiInfos;
    }

    /**
     * Parse the string representation.
     */
    private static Map parse(String specification) {
        if (specification == null) {
            return Collections.emptyMap();
        }
        var result = new HashMap();
        for (var prop : specification.split(COMMA)) {
            prop = prop.strip();
            if (prop.isEmpty()) {
                continue;
            }
            var keyValue = prop.split("=", 2);
            var key = keyValue[0].strip();
            var value = keyValue.length == 1 ? StringUtils.EMPTY_STRING : keyValue[1].strip();
            if (key.isEmpty()) {
                continue;
            }
            result.put(key, value);
        }
        return result;
    }

    /**
     * Creates an OpenApiViewConfig form a String representation.
     *
     * @param specification A String representation of an OpenApiViewConfig.
     * @param openApiInfos Open API info objects.
     * @param openApiProperties The open api properties.
     * @param context Visitor context.
     *
     * @return An OpenApiViewConfig.
     */
    public static OpenApiViewConfig fromSpecification(String specification, Map, OpenApiInfo> openApiInfos, Properties openApiProperties, VisitorContext context) {
        var openApiMap = new HashMap(openApiProperties.size());
        openApiProperties.forEach((key, value) -> openApiMap.put((String) key, (String) value));
        openApiMap.putAll(parse(specification));
        var cfg = new OpenApiViewConfig(openApiInfos);
        RapiPDFConfig rapiPDFConfig = RapiPDFConfig.fromProperties(openApiMap, openApiInfos, context);
        if ("true".equals(openApiMap.getOrDefault("redoc.enabled", Boolean.FALSE.toString()))) {
            cfg.redocConfig = RedocConfig.fromProperties(openApiMap, openApiInfos, context);
            cfg.redocConfig.rapiPDFConfig = rapiPDFConfig;
        }
        if ("true".equals(openApiMap.getOrDefault("rapidoc.enabled", Boolean.FALSE.toString()))) {
            cfg.rapidocConfig = RapidocConfig.fromProperties(openApiMap, openApiInfos, context);
            cfg.rapidocConfig.rapiPDFConfig = rapiPDFConfig;
        }
        if ("true".equals(openApiMap.getOrDefault("openapi-explorer.enabled", Boolean.FALSE.toString()))) {
            cfg.openApiExplorerConfig = OpenApiExplorerConfig.fromProperties(openApiMap, openApiInfos, context);
            cfg.openApiExplorerConfig.rapiPDFConfig = rapiPDFConfig;
        }
        if ("true".equals(openApiMap.getOrDefault("swagger-ui.enabled", Boolean.FALSE.toString()))) {
            cfg.swaggerUIConfig = SwaggerUIConfig.fromProperties(openApiMap, openApiInfos, context);
            cfg.swaggerUIConfig.rapiPDFConfig = rapiPDFConfig;
        }
        cfg.mappingPath = openApiMap.getOrDefault("mapping.path", DEFAULT_SPEC_MAPPING_PATH);
        return cfg;
    }

    /**
     * Returns true when the generation of views is enabled.
     *
     * @return true when the generation of views is enabled.
     */
    public boolean isEnabled() {
        return redocConfig != null || rapidocConfig != null || swaggerUIConfig != null || openApiExplorerConfig != null;
    }

    /**
     * Generates the views given this configuration.
     *
     * @param outputDir The destination directory of the generated views.
     * @param context The visitor context
     *
     * @throws IOException When the generation fails.
     */
    public void render(Path outputDir, VisitorContext context) throws IOException {
        if (redocConfig != null) {
            copyResources(outputDir, context, REDOC, TEMPLATES_REDOC, redocConfig, redocConfig.rapiPDFConfig);
        }
        if (rapidocConfig != null) {
            copyResources(outputDir, context, RAPIDOC, TEMPLATES_RAPIDOC, rapidocConfig, rapidocConfig.rapiPDFConfig);
        }
        if (openApiExplorerConfig != null) {
            Path openapiExplorerDir = outputDir.resolve(OPENAPI_EXPLORER);
            render(openApiExplorerConfig, openapiExplorerDir, TEMPLATES + SLASH + TEMPLATES_OPENAPI_EXPLORER + SLASH + TEMPLATE_INDEX_HTML, context);
            copyResources(openApiExplorerConfig, openapiExplorerDir, TEMPLATES_OPENAPI_EXPLORER, openApiExplorerConfig.getResources(), context);
            if (openApiExplorerConfig.rapiPDFConfig.enabled) {
                copyResources(openApiExplorerConfig.rapiPDFConfig, openapiExplorerDir, TEMPLATES_RAPIPDF, openApiExplorerConfig.rapiPDFConfig.getResources(), context);
            }
        }
        if (swaggerUIConfig != null) {
            Path swaggerUiDir = copyResources(outputDir, context, SWAGGER_UI, TEMPLATES_SWAGGER_UI, swaggerUIConfig, swaggerUIConfig.rapiPDFConfig);
            if (SwaggerUIConfig.hasOauth2Option(swaggerUIConfig.options)) {
                render(swaggerUIConfig, swaggerUiDir, TEMPLATES + SLASH + TEMPLATES_SWAGGER_UI + SLASH + TEMPLATE_OAUTH_2_REDIRECT_HTML, context);
            }
            copySwaggerUiTheme(swaggerUIConfig, swaggerUiDir, TEMPLATES_SWAGGER_UI, context);
        }
    }

    private Path copyResources(@NonNull Path outputDir,
                               @NonNull VisitorContext context,
                               @NonNull String otherDir,
                               @NonNull String templates,
                               AbstractViewConfig viewConfig,
                               AbstractViewConfig rapidPDFConfig) throws IOException {
        Path dir = outputDir.resolve(otherDir);
        render(viewConfig, dir, TEMPLATES + SLASH + templates + SLASH + TEMPLATE_INDEX_HTML, context);
        copyResources(viewConfig, dir, templates, viewConfig.getResources(), context);
        if (rapidPDFConfig.isEnabled()) {
            copyResources(rapidPDFConfig, dir, TEMPLATES_RAPIPDF, rapidPDFConfig.getResources(), context);
        }
        return dir;
    }

    private void copySwaggerUiTheme(SwaggerUIConfig cfg, Path outputDir, String templatesDir, VisitorContext context) throws IOException {

        if (!cfg.copyTheme) {
            return;
        }

        String themeFileName = cfg.theme.getCss() + ".css";

        Path resDir = outputDir.resolve(RESOURCE_DIR);
        if (!Files.exists(resDir)) {
            Files.createDirectories(resDir);
        }

        ClassLoader classLoader = getClass().getClassLoader();
        try (var is = classLoader.getResourceAsStream(TEMPLATES + SLASH + templatesDir + SLASH + THEMES_DIR + SLASH + themeFileName)) {

            Files.copy(is, Paths.get(resDir.toString(), themeFileName), StandardCopyOption.REPLACE_EXISTING);
            Path file = resDir.resolve(themeFileName);
            if (context != null) {
                info("Writing OpenAPI View Resources to destination: " + file, context);
                var classesOutputPath = ContextUtils.getClassesOutputPath(context);
                if (classesOutputPath != null) {
                    // add relative path for the file, so that the micronaut-graal visitor knows about it
                    addGeneratedResource(classesOutputPath.relativize(file).toString(), context);
                }
            }
        } catch (Exception e) {
            warn("Can't copy resource: " + themeFileName, context);
            throw new RuntimeException(e);
        }
    }

    private void copyResources(AbstractViewConfig cfg, Path outputDir, String templateDir, List resources, VisitorContext context) throws IOException {
        if (!cfg.copyResources) {
            return;
        }

        ClassLoader classLoader = getClass().getClassLoader();

        Path outputResDir = outputDir.resolve(RESOURCE_DIR);
        if (!Files.exists(outputResDir)) {
            Files.createDirectories(outputResDir);
        }

        if (CollectionUtils.isNotEmpty(resources)) {
            for (String resource : resources) {
                try (var is = classLoader.getResourceAsStream(TEMPLATES + SLASH + templateDir + SLASH + resource)) {

                    Files.copy(is, Paths.get(outputDir.toString(), resource), StandardCopyOption.REPLACE_EXISTING);
                    Path file = outputResDir.resolve(resource);

                    if (context != null) {
                        info("Writing OpenAPI View Resources to destination: " + file, context);
                        var classesOutputPath = ContextUtils.getClassesOutputPath(context);
                        if (classesOutputPath != null) {
                            // add relative path for the file, so that the micronaut-graal visitor knows about it
                            addGeneratedResource(classesOutputPath.relativize(file).toString(), context);
                        }
                    }
                } catch (Exception e) {
                    warn("Can't copy resource: " + resource, context);
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private String readTemplateFromClasspath(String templateName) throws IOException {
        ClassLoader classLoader = getClass().getClassLoader();
        try (var in = classLoader.getResourceAsStream(templateName);
             var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
        ) {
            return readFile(reader);
        } catch (Exception e) {
            throw new IOException("Fail to load " + templateName, e);
        }
    }

    private String readTemplateFromCustomPath(String customPathStr, @Nullable VisitorContext context) throws IOException {
        String projectDir = StringUtils.EMPTY_STRING;
        Path projectPath = context != null ? getProjectPath(context) : null;
        if (projectPath != null) {
            projectDir = projectPath.toString().replace("\\\\", SLASH);
        }
        if (customPathStr.startsWith("project:")) {
            customPathStr = customPathStr.replace("project:", projectDir);
        } else if (!customPathStr.startsWith("file:") && !customPathStr.startsWith("classpath:")) {
            if (!projectDir.endsWith(SLASH)) {
                projectDir += SLASH;
            }
            if (customPathStr.startsWith(SLASH)) {
                customPathStr = customPathStr.substring(1);
            }
            customPathStr = projectDir + customPathStr;
        } else if (customPathStr.startsWith("file:")) {
            customPathStr = customPathStr.substring(5);
        } else if (customPathStr.startsWith("classpath:")) {
            var resourceLoader = new DefaultClassPathResourceLoader(getClass().getClassLoader());
            Optional inOpt = resourceLoader.getResourceAsStream(customPathStr);
            if (inOpt.isEmpty()) {
                throw new IOException("Fail to load " + customPathStr);
            }
            try (InputStream in = inOpt.get();
                 var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
            ) {
                return readFile(reader);
            } catch (IOException e) {
                throw new IOException("Fail to load " + customPathStr, e);
            }
        }

        Path templatePath = resolve(context, Paths.get(customPathStr));
        if (!Files.isReadable(templatePath)) {
            throw new IOException("Can't read file " + customPathStr);
        }
        try (BufferedReader reader = Files.newBufferedReader(templatePath)) {
            return readFile(reader);
        } catch (IOException e) {
            throw new IOException("Fail to load " + customPathStr, e);
        }
    }

    private void render(AbstractViewConfig cfg, Path outputDir, String templateName, @Nullable VisitorContext context) throws IOException {

        String template;
        if (StringUtils.isEmpty(cfg.templatePath)) {
            template = readTemplateFromClasspath(templateName);
        } else {
            template = readTemplateFromCustomPath(cfg.templatePath, context);
        }

        template = cfg.render(template, context);
        template = replacePlaceHolder(template, "specURL", getSpecURL(cfg, context), StringUtils.EMPTY_STRING);
        template = replacePlaceHolder(template, "title", title, StringUtils.EMPTY_STRING);
        if (!Files.exists(outputDir)) {
            Files.createDirectories(outputDir);
        }
        String fileName = templateName.substring(templateName.lastIndexOf(SLASH) + 1);
        Path file = outputDir.resolve(fileName);
        info("Writing OpenAPI View to destination: " + file, context);
        var classesOutputPath = ContextUtils.getClassesOutputPath(context);
        if (classesOutputPath != null) {
            // add relative path for the file, so that the micronaut-graal visitor knows about it
            addGeneratedResource(classesOutputPath.relativize(file).toString(), context);
        }
        try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8,
            StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
        ) {
            writer.write(template);
        }
    }

    /**
     * Returns the title for the generated views.
     *
     * @return A title.
     */
    public String getTitle() {
        return title;
    }

    /**
     * Sets the title for the generated views.
     *
     * @param title A title.
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Returns the relative openApi specification url path.
     *
     * @param cfg view config.
     * @param context Visitor context.
     *
     * @return A path.
     */
    public String getSpecURL(AbstractViewConfig cfg, @Nullable VisitorContext context) {

        if (cfg.specUrl != null) {
            return cfg.specUrl;
        }
        if (specFile == null) {
            return StringUtils.EMPTY_STRING;
        }

        // process micronaut.openapi.server.context.path
        String serverContextPath = ConfigUtils.getConfigProperty(MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH, context);
        if (serverContextPath == null) {
            serverContextPath = StringUtils.EMPTY_STRING;
        }
        String finalUrl = serverContextPath.startsWith(SLASH) ? serverContextPath : SLASH + serverContextPath;
        if (!finalUrl.endsWith(SLASH)) {
            finalUrl += SLASH;
        }

        // process micronaut.server.context-path
        String contextPath = ConfigUtils.getServerContextPath(context);
        finalUrl += contextPath.startsWith(SLASH) ? contextPath.substring(1) : contextPath;
        if (!finalUrl.endsWith(SLASH)) {
            finalUrl += SLASH;
        }

        finalUrl = StringUtils.prependUri(finalUrl, StringUtils.prependUri(mappingPath, specFile));
        if (!finalUrl.startsWith(SLASH) && !finalUrl.startsWith(DOLLAR)) {
            finalUrl = SLASH + finalUrl;
        }
        return finalUrl;
    }

    /**
     * Sets the generated openApi specification file name.
     *
     * @param specFile The openApi specification file name.
     */
    public void setSpecFile(String specFile) {
        this.specFile = specFile;
    }

    /**
     * Replaces placeholders in the template.
     *
     * @param template A template.
     * @param placeHolder The placeholder to replace.
     * @param value The value that will replace the placeholder.
     * @param valuePrefix A prefix.
     *
     * @return The updated template.
     */
    static String replacePlaceHolder(String template, String placeHolder, String value, String valuePrefix) {
        return template.replace("{{" + placeHolder + "}}", StringUtils.isNotEmpty(value) ? valuePrefix + value : StringUtils.EMPTY_STRING);
    }

    public SwaggerUIConfig getSwaggerUIConfig() {
        return swaggerUIConfig;
    }

    public RedocConfig getRedocConfig() {
        return redocConfig;
    }

    public RapidocConfig getRapidocConfig() {
        return rapidocConfig;
    }

    public OpenApiExplorerConfig getOpenApiExplorerConfig() {
        return openApiExplorerConfig;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy