
io.micronaut.openapi.visitor.OpenApiApplicationVisitor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-openapi Show documentation
Show all versions of micronaut-openapi Show documentation
Configuration to integrate Micronaut and OpenAPI/Swagger
The newest version!
/*
* Copyright 2017-2020 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.databind.JsonNode;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.micronaut.annotation.processing.visitor.JavaClassElementExt;
import io.micronaut.context.env.Environment;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.util.StringUtils;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.inject.writer.GeneratedFile;
import io.micronaut.openapi.view.OpenApiViewConfig;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityScheme;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import javax.annotation.processing.SupportedOptions;
/**
* Visits the application class.
*
* @author graemerocher
* @since 1.0
*/
@Experimental
@SupportedOptions({
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_VIEWS_SPEC,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_TARGET_FILE,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_ADDITIONAL_FILES,
OpenApiApplicationVisitor.MICRONAUT_OPENAPI_CONFIG_FILE,
})
public class OpenApiApplicationVisitor extends AbstractOpenApiVisitor implements TypeElementVisitor {
/**
* System property that enables setting the open api config file.
*/
public static final String MICRONAUT_OPENAPI_CONFIG_FILE = "micronaut.openapi.config.file";
/**
* Prefix for expandable properties.
*/
public static final String MICRONAUT_OPENAPI_EXPAND_PREFIX = "micronaut.openapi.expand.";
/**
* System property for server context path.
*/
public static final String MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH = "micronaut.openapi.server.context.path";
/**
* System property for naming strategy. One jackson PropertyNamingStrategy.
*/
public static final String MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY = "micronaut.openapi.property.naming.strategy";
/**
* System property for views specification.
*/
public static final String MICRONAUT_OPENAPI_VIEWS_SPEC = "micronaut.openapi.views.spec";
/**
* System property that enables setting the target file to write to.
*/
public static final String MICRONAUT_OPENAPI_TARGET_FILE = "micronaut.openapi.target.file";
/**
* System property that specifies the location of additional swagger YAML files to read from.
*/
public static final String MICRONAUT_OPENAPI_ADDITIONAL_FILES = "micronaut.openapi.additional.files";
/**
* Default openapi config file.
*/
public static final String OPENAPI_CONFIG_FILE = "openapi.properties";
/**
* The name of the entry for Endpoint class tags in the context.
*/
public static final String MICRONAUT_OPENAPI_ENDPOINT_CLASS_TAGS = "micronaut.openapi.endpoint.class.tags";
/**
* The name of the entry for Endpoint servers in the context.
*/
public static final String MICRONAUT_OPENAPI_ENDPOINT_SERVERS = "micronaut.openapi.endpoint.servers";
/**
* The name of the entry for Endpoint security requirements in the context.
*/
public static final String MICRONAUT_OPENAPI_ENDPOINT_SECURITY_REQUIREMENTS = "micronaut.openapi.endpoint.security.requirements";
private static final String MICRONAUT_OPENAPI_PROJECT_DIR = "micronaut.openapi.project.dir";
private static final String MICRONAUT_OPENAPI_PROPERTIES = "micronaut.openapi.properties";
private static final String MICRONAUT_OPENAPI_ENDPOINTS = "micronaut.openapi.endpoints";
private ClassElement classElement;
private int visitedElements = -1;
@Override
public void visitClass(ClassElement element, VisitorContext context) {
incrementVisitedElements(context);
context.info("Generating OpenAPI Documentation");
OpenAPI openAPI = readOpenAPI(element, context);
mergeAdditionalSwaggerFiles(element, context, openAPI);
// handle type level tags
List tagList = processOpenApiAnnotation(
element,
context,
Tag.class,
io.swagger.v3.oas.models.tags.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,
Server.class,
io.swagger.v3.oas.models.servers.Server.class,
openAPI.getServers()
);
openAPI.setServers(servers);
// Handle Application securityRequirements schemes
processSecuritySchemes(element, context);
Optional attr = context.get(ATTR_OPENAPI, OpenAPI.class);
if (attr.isPresent()) {
OpenAPI existing = attr.get();
Optional.ofNullable(openAPI.getInfo())
.ifPresent(existing::setInfo);
copyOpenAPI(existing, openAPI);
} else {
context.put(ATTR_OPENAPI, openAPI);
}
if (Boolean.getBoolean(ATTR_TEST_MODE)) {
resolveOpenAPI(context);
}
this.classElement = element;
}
private String getConfigurationProperty(String key, VisitorContext context) {
return System.getProperty(key, readOpenApiConfigFile(context).getProperty(key));
}
/**
* Merge the OpenAPI YAML 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 = getConfigurationProperty(MICRONAUT_OPENAPI_ADDITIONAL_FILES, context);
if (StringUtils.isNotEmpty(additionalSwaggerFiles)) {
Path directory = resolve(context, Paths.get(additionalSwaggerFiles));
if (Files.isDirectory(directory)) {
context.info("Merging Swagger OpenAPI YAML files from location: " + directory);
try (DirectoryStream stream = Files.newDirectoryStream(directory, path -> path.toString().endsWith(".yml"))) {
stream.forEach(path -> {
context.info("Reading Swagger OpenAPI YAML file " + path.getFileName());
OpenAPI parsedOpenApi = null;
try {
parsedOpenApi = yamlMapper.readValue(path.toFile(), OpenAPI.class);
} catch (IOException e) {
context.warn("Unable to read file " + path.getFileName() + ": " + e.getMessage(), element);
}
copyOpenAPI(openAPI, parsedOpenApi);
});
} catch (IOException e) {
context.warn("Unable to read file from " + directory + ": " + e.getMessage(), element);
}
} else {
context.warn(directory + " does not exist or is not a directory", element);
}
}
}
private static Path resolve(VisitorContext context, Path path) {
if (!path.isAbsolute()) {
Path projectDir = projectDir(context);
if (projectDir != null) {
path = projectDir.resolve(path);
}
}
return path.toAbsolutePath();
}
/**
* Returns the EndpointsConfiguration.
* @param context The context.
* @return The EndpointsConfiguration.
*/
static EndpointsConfiguration endPointsConfiguration(VisitorContext context) {
Optional cfg = context.get(MICRONAUT_OPENAPI_ENDPOINTS, EndpointsConfiguration.class);
if (cfg.isPresent()) {
return cfg.get();
}
EndpointsConfiguration conf = new EndpointsConfiguration(context, readOpenApiConfigFile(context));
context.put(MICRONAUT_OPENAPI_ENDPOINTS, conf);
return conf;
}
private static Properties readOpenApiConfigFile(VisitorContext context) {
Optional props = context.get(MICRONAUT_OPENAPI_PROPERTIES, Properties.class);
if (props.isPresent()) {
return props.get();
}
Properties openApiProperties = new Properties();
String cfgFile = System.getProperty(MICRONAUT_OPENAPI_CONFIG_FILE, OPENAPI_CONFIG_FILE);
if (StringUtils.isNotEmpty(cfgFile)) {
Path cfg = resolve(context, Paths.get(cfgFile));
if (Files.isReadable(cfg)) {
try (Reader reader = Files.newBufferedReader(cfg)) {
openApiProperties.load(reader);
} catch (IOException e) {
context.warn("Fail to read OpenAPI configuration file: " + e.getMessage(), null);
}
} else if (Files.exists(cfg)) {
context.warn("Can not read configuration file: " + cfg, null);
}
}
context.put(MICRONAUT_OPENAPI_PROPERTIES, openApiProperties);
return openApiProperties;
}
/**
* Copy information from one {@link OpenAPI} object to another.
*
* @param to The {@link OpenAPI} object to copy to
* @param from The {@link OpenAPI} object to copy from
*/
private void copyOpenAPI(OpenAPI to, OpenAPI from) {
if (to != null && from != null) {
Optional.ofNullable(from.getTags()).ifPresent(tags -> tags.forEach(to::addTagsItem));
Optional.ofNullable(from.getServers()).ifPresent(servers -> servers.forEach(to::addServersItem));
Optional.ofNullable(from.getSecurity()).ifPresent(securityRequirements -> securityRequirements.forEach(to::addSecurityItem));
Optional.ofNullable(from.getPaths()).ifPresent(paths -> paths.forEach(to::path));
Optional.ofNullable(from.getComponents()).ifPresent(components -> {
Map schemas = components.getSchemas();
if (schemas != null && !schemas.isEmpty()) {
schemas.forEach((k, v) -> {
if (v.getName() == null) {
v.setName(k);
}
});
schemas.forEach(to::schema);
}
Map securitySchemes = components.getSecuritySchemes();
if (securitySchemes != null && !securitySchemes.isEmpty()) {
securitySchemes.forEach(to::schemaRequirement);
}
});
Optional.ofNullable(from.getExternalDocs()).ifPresent(to::externalDocs);
Optional.ofNullable(from.getExtensions()).ifPresent(extensions -> extensions.forEach(to::addExtension));
}
}
private OpenAPI readOpenAPI(ClassElement element, VisitorContext context) {
return element.findAnnotation(OpenAPIDefinition.class).flatMap(o -> {
Optional result = toValue(o.getValues(), context, OpenAPI.class);
result.ifPresent(openAPI -> {
List securityRequirements =
o.getAnnotations("security", io.swagger.v3.oas.annotations.security.SecurityRequirement.class)
.stream()
.map(this::mapToSecurityRequirement)
.collect(Collectors.toList());
openAPI.setSecurity(securityRequirements);
});
return result;
}).orElse(new OpenAPI());
}
private void renderViews(String title, String specFile, Path destinationDir, VisitorContext visitorContext) throws IOException {
String viewSpecification = System.getProperty(MICRONAUT_OPENAPI_VIEWS_SPEC);
OpenApiViewConfig cfg = OpenApiViewConfig.fromSpecification(viewSpecification, readOpenApiConfigFile(visitorContext));
if (cfg.isEnabled()) {
cfg.setTitle(title);
cfg.setSpecFile(specFile);
cfg.setServerContextPath(getConfigurationProperty(MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH, visitorContext));
cfg.render(destinationDir.resolve("views"), visitorContext);
}
}
private static PropertyNamingStrategyBase fromName(String name) {
if (name == null) {
return null;
}
switch (name.toUpperCase(Locale.US)) {
case "SNAKE_CASE": return (PropertyNamingStrategyBase) PropertyNamingStrategy.SNAKE_CASE;
case "UPPER_CAMEL_CASE": return (PropertyNamingStrategyBase) PropertyNamingStrategy.UPPER_CAMEL_CASE;
case "LOWER_CAMEL_CASE": return new LowerCamelCasePropertyNamingStrategy();
case "LOWER_CASE": return (PropertyNamingStrategyBase) PropertyNamingStrategy.LOWER_CASE;
case "KEBAB_CASE": return (PropertyNamingStrategyBase) PropertyNamingStrategy.KEBAB_CASE;
default: return null;
}
}
private Optional openApiSpecFile(String fileName, VisitorContext visitorContext) {
Optional path = userDefinedSpecFile(visitorContext);
if (path.isPresent()) {
return path;
}
// default location
Optional generatedFile = visitorContext.visitMetaInfFile("swagger/" + fileName);
if (generatedFile.isPresent()) {
URI uri = generatedFile.get().toURI();
// happens in tests 'mem:///CLASS_OUTPUT/META-INF/swagger/swagger.yml'
if (uri.getScheme() != null && !uri.getScheme().equals("mem")) {
Path specPath = Paths.get(uri);
createDirectories(specPath, visitorContext);
return Optional.of(specPath);
}
}
visitorContext.warn("Unable to get swagger/" + fileName + " file.", null);
return Optional.empty();
}
private static Path projectDir(VisitorContext visitorContext) {
Optional projectDir = visitorContext.get(MICRONAUT_OPENAPI_PROJECT_DIR, Path.class);
if (projectDir.isPresent()) {
return projectDir.get();
}
// let's find the projectDir
Optional dummyFile = visitorContext.visitGeneratedFile("dummy");
if (dummyFile.isPresent()) {
URI uri = dummyFile.get().toURI();
// happens in tests 'mem:///CLASS_OUTPUT/META-INF/swagger/swagger.yml'
if (uri.getScheme() != null && !uri.getScheme().equals("mem")) {
// assume files are generated in 'build' dir
Path dummy = Paths.get(uri).normalize();
while (dummy != null) {
Path dummyFileName = dummy.getFileName();
if (dummyFileName != null && "build".equals(dummyFileName.toString())) {
projectDir = Optional.ofNullable(dummy.getParent());
visitorContext.put(MICRONAUT_OPENAPI_PROJECT_DIR, dummy.getParent());
break;
}
dummy = dummy.getParent();
}
}
}
return projectDir.isPresent() ? projectDir.get() : null;
}
private Optional userDefinedSpecFile(VisitorContext visitorContext) {
String targetFile = getConfigurationProperty(MICRONAUT_OPENAPI_TARGET_FILE, visitorContext);
if (StringUtils.isEmpty(targetFile)) {
return Optional.empty();
}
Path specFile = resolve(visitorContext, Paths.get(targetFile));
createDirectories(specFile, visitorContext);
return Optional.of(specFile);
}
private static void createDirectories(Path f, VisitorContext visitorContext) {
if (f.getParent() != null) {
try {
Files.createDirectories(f.getParent());
} catch (IOException e) {
visitorContext.warn("Fail to create directories for" + f + ": " + e.getMessage(), null);
}
}
}
private void applyPropertyNamingStrategy(OpenAPI openAPI, VisitorContext visitorContext) {
final String namingStrategyName = getConfigurationProperty(MICRONAUT_OPENAPI_PROPERTY_NAMING_STRATEGY, visitorContext);
final PropertyNamingStrategyBase propertyNamingStrategy = fromName(namingStrategyName);
if (propertyNamingStrategy != null) {
visitorContext.info("Using " + namingStrategyName + " property naming strategy.");
openAPI.getComponents().getSchemas().values().forEach(model -> {
Map properties = model.getProperties();
if (properties != null) {
Map newProperties = properties.entrySet().stream()
.collect(Collectors.toMap(entry -> propertyNamingStrategy.translate(entry.getKey()),
Map.Entry::getValue, (prop1, prop2) -> prop1, LinkedHashMap::new));
model.getProperties().clear();
model.setProperties(newProperties);
}
List required = model.getRequired();
if (required != null) {
List updatedRequired = required.stream().map(propertyNamingStrategy::translate).collect(Collectors.toList());
required.clear();
required.addAll(updatedRequired);
}
});
}
}
private void applyPropertyServerContextPath(OpenAPI openAPI, VisitorContext visitorContext) {
final String serverContextPath = getConfigurationProperty(MICRONAUT_OPENAPI_CONTEXT_SERVER_PATH, visitorContext);
if (serverContextPath == null || serverContextPath.isEmpty()) {
return;
}
visitorContext.info("Applying server context path: " + serverContextPath + " to Paths.");
io.swagger.v3.oas.models.Paths paths = openAPI.getPaths();
if (paths == null || paths.isEmpty()) {
return;
}
final io.swagger.v3.oas.models.Paths newPaths = new io.swagger.v3.oas.models.Paths();
for (Map.Entry path: paths.entrySet()) {
final String mapping = path.getKey();
newPaths.addPathItem(mapping.startsWith(serverContextPath) ? mapping : StringUtils.prependUri(serverContextPath, mapping), path.getValue());
}
openAPI.setPaths(newPaths);
}
private JsonNode resolvePlaceholders(ArrayNode anode, UnaryOperator propertyExpander) {
for (int i = 0 ; i < anode.size(); ++i) {
anode.set(i, resolvePlaceholders(anode.get(i), propertyExpander));
}
return anode;
}
private JsonNode resolvePlaceholders(ObjectNode onode, UnaryOperator propertyExpander) {
if (onode.size() == 0) {
return onode;
}
final ObjectNode newNode = onode.objectNode();
for (Iterator> i = onode.fields(); i.hasNext();) {
final Map.Entry entry = i.next();
newNode.set(propertyExpander.apply(entry.getKey()), resolvePlaceholders(entry.getValue(), propertyExpander));
}
return newNode;
}
private JsonNode resolvePlaceholders(JsonNode node, UnaryOperator propertyExpander) {
if (node.isTextual()) {
final String text = node.textValue();
if (text == null || text.trim().isEmpty()) {
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;
}
}
private String expandProperties(String s, List> properties) {
if (s == null || s.isEmpty()) {
return s;
}
for (Map.Entry entry: properties) {
s = s.replace(entry.getKey(), entry.getValue());
}
return s;
}
private OpenAPI resolvePropertyPlaceHolders(OpenAPI openAPI, VisitorContext visitorContext) {
List> expandableProperties = readOpenApiConfigFile(visitorContext).entrySet()
.stream()
.filter(entry -> entry.getKey().toString().startsWith(MICRONAUT_OPENAPI_EXPAND_PREFIX))
.map(entry -> new AbstractMap.SimpleImmutableEntry<>("${" + entry.getKey().toString().substring(MICRONAUT_OPENAPI_EXPAND_PREFIX.length()) + '}', entry.getValue().toString())).collect(Collectors.toList());
if (expandableProperties.isEmpty()) {
return openAPI;
}
visitorContext.info("Expanding properties: " + expandableProperties);
JsonNode root = resolvePlaceholders(Yaml.mapper().convertValue(openAPI, ObjectNode.class), s -> expandProperties(s, expandableProperties));
return Yaml.mapper().convertValue(root, OpenAPI.class);
}
@Override
public void finish(VisitorContext visitorContext) {
if (visitedElements == visitedElements(visitorContext)) {
// nothing new visited, avoid rewriting the files.
return;
}
Optional attr = visitorContext.get(ATTR_OPENAPI, OpenAPI.class);
if (!attr.isPresent()) {
return;
}
OpenAPI openAPI = attr.get();
processEndpoints(visitorContext);
applyPropertyNamingStrategy(openAPI, visitorContext);
applyPropertyServerContextPath(openAPI, visitorContext);
openAPI = resolvePropertyPlaceHolders(openAPI, visitorContext);
String fileName = "swagger.yml";
String documentTitle = "OpenAPI";
Info info = openAPI.getInfo();
if (info != null) {
documentTitle = Optional.ofNullable(info.getTitle()).orElse(Environment.DEFAULT_NAME);
documentTitle = documentTitle.toLowerCase(Locale.US).replace(' ', '-');
String version = info.getVersion();
if (version != null) {
documentTitle = documentTitle + '-' + version;
}
fileName = documentTitle + ".yml";
}
Optional specFile = openApiSpecFile(fileName, visitorContext);
if (specFile.isPresent()) {
Path specPath = specFile.get();
try (Writer writer = Files.newBufferedWriter(specPath, StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.CREATE)) {
visitorContext.info("Writing OpenAPI YAML to destination: " + specPath);
yamlMapper.writeValue(writer, openAPI);
renderViews(documentTitle, specPath.getFileName().toString(), specPath.getParent(), visitorContext);
} catch (Exception e) {
visitorContext.warn("Unable to generate swagger.yml: " + specPath + " - " + e.getMessage(),
classElement);
}
}
visitedElements = visitedElements(visitorContext);
}
private void processEndpoints(VisitorContext visitorContext) {
EndpointsConfiguration endpointsCfg = endPointsConfiguration(visitorContext);
if ("io.micronaut.annotation.processing.visitor.JavaVisitorContext".equals(visitorContext.getClass().getName())
&& endpointsCfg.isEnabled()
&& ! endpointsCfg.getEndpoints().isEmpty()) {
OpenApiEndpointVisitor visitor = new OpenApiEndpointVisitor();
endpointsCfg.getEndpoints().values().stream()
.filter(endpoint -> endpoint.getClassElement().isPresent()
&& isJavaElement(endpoint.getClassElement().get(), visitorContext))
.forEach(endpoint -> {
ClassElement element = endpoint.getClassElement().get();
visitorContext.put(MICRONAUT_OPENAPI_ENDPOINT_CLASS_TAGS, endpoint.getTags());
visitorContext.put(MICRONAUT_OPENAPI_ENDPOINT_SERVERS, endpoint.getServers());
visitorContext.put(MICRONAUT_OPENAPI_ENDPOINT_SECURITY_REQUIREMENTS, endpoint.getSecurityRequirements());
visitor.visitClass(element, visitorContext);
JavaClassElementExt javaClassElement = new JavaClassElementExt(element, visitorContext);
javaClassElement.getCandidateMethods().forEach(method -> visitor.visitMethod(method, visitorContext));
});
}
}
static class LowerCamelCasePropertyNamingStrategy extends PropertyNamingStrategyBase {
private static final long serialVersionUID = -2750503285679998670L;
@Override
public String translate(String propertyName) {
return propertyName;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy