Skip to content

Commit

Permalink
Merge pull request #19148 from phillip-kruger/swagger-ui-enhancements
Browse files Browse the repository at this point in the history
Small OpenAPI and Swagger UI enhancements
  • Loading branch information
phillip-kruger authored Aug 2, 2021
2 parents a01f9ef + 779dd5a commit 95bcc1e
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 41 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<smallrye-config.version>2.4.2</smallrye-config.version>
<smallrye-health.version>3.1.1</smallrye-health.version>
<smallrye-metrics.version>3.0.1</smallrye-metrics.version>
<smallrye-open-api.version>2.1.8</smallrye-open-api.version>
<smallrye-open-api.version>2.1.9</smallrye-open-api.version>
<smallrye-graphql.version>1.3.0</smallrye-graphql.version>
<smallrye-opentracing.version>2.0.1</smallrye-opentracing.version>
<smallrye-fault-tolerance.version>5.2.1</smallrye-fault-tolerance.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.smallrye.openapi.common.deployment;

import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
Expand Down Expand Up @@ -33,6 +34,13 @@ public final class SmallRyeOpenApiConfig {
@ConfigItem(defaultValue = "false")
public boolean ignoreStaticDocument;

/**
* A list of local directories that should be scanned for yaml and/or json files to be included in the static model.
* Example: `META-INF/openapi/`
*/
@ConfigItem
public Optional<List<Path>> additionalDocsDirectory;

/**
* Add a certain SecurityScheme with config
*/
Expand All @@ -50,6 +58,12 @@ public final class SmallRyeOpenApiConfig {
@ConfigItem(defaultValue = "Authentication")
public String securitySchemeDescription;

/**
* This will automatically add the security requirement to all methods/classes that has a `RolesAllowed` annotation.
*/
@ConfigItem(defaultValue = "true")
public boolean autoAddSecurityRequirement;

/**
* Add a scheme value to the Basic HTTP Security Scheme
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.quarkus.smallrye.openapi.deployment;

import java.io.BufferedReader;
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;
Expand All @@ -10,9 +12,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -23,13 +28,16 @@
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.CompositeIndex;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

Expand Down Expand Up @@ -74,14 +82,19 @@
import io.smallrye.openapi.api.OpenApiConfig;
import io.smallrye.openapi.api.OpenApiConfigImpl;
import io.smallrye.openapi.api.OpenApiDocument;
import io.smallrye.openapi.api.constants.SecurityConstants;
import io.smallrye.openapi.api.models.OpenAPIImpl;
import io.smallrye.openapi.api.util.MergeUtil;
import io.smallrye.openapi.jaxrs.JaxRsConstants;
import io.smallrye.openapi.runtime.OpenApiProcessor;
import io.smallrye.openapi.runtime.OpenApiStaticFile;
import io.smallrye.openapi.runtime.io.Format;
import io.smallrye.openapi.runtime.io.OpenApiSerializer;
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner;
import io.smallrye.openapi.runtime.util.JandexUtil;
import io.smallrye.openapi.spring.SpringConstants;
import io.smallrye.openapi.vertx.VertxConstants;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
Expand Down Expand Up @@ -182,14 +195,6 @@ RouteBuildItem handler(LaunchModeBuildItem launch,
.build();
}

@BuildStep
void addSecurityFilter(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPIDefinitionProducer,
SmallRyeOpenApiConfig config) {

addToOpenAPIDefinitionProducer
.produce(new AddToOpenAPIDefinitionBuildItem(new SecurityConfigFilter(config)));
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void classLoaderHack(OpenApiRecorder recorder) {
Expand All @@ -214,6 +219,48 @@ OpenApiFilteredIndexViewBuildItem smallryeOpenApiIndex(CombinedIndexBuildItem co
new OpenApiConfigImpl(ConfigProvider.getConfig())));
}

@BuildStep
void addSecurityFilter(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPIDefinitionProducer,
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
SmallRyeOpenApiConfig config) {

List<AnnotationInstance> rolesAllowedAnnotations = new ArrayList<>();
for (DotName rolesAllowed : SecurityConstants.ROLES_ALLOWED) {
rolesAllowedAnnotations.addAll(apiFilteredIndexViewBuildItem.getIndex().getAnnotations(rolesAllowed));
}

Map<String, List<String>> methodReferences = new HashMap<>();
DotName securityRequirement = DotName.createSimple(SecurityRequirement.class.getName());
for (AnnotationInstance ai : rolesAllowedAnnotations) {
if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) {
MethodInfo method = ai.target().asMethod();
if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) {
String ref = JandexUtil.createUniqueMethodReference(method);
methodReferences.put(ref, List.of(ai.value().asStringArray()));
}
}
if (ai.target().kind().equals(AnnotationTarget.Kind.CLASS)) {
ClassInfo classInfo = ai.target().asClass();
List<MethodInfo> methods = classInfo.methods();
for (MethodInfo method : methods) {
if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) {
String ref = JandexUtil.createUniqueMethodReference(method);
methodReferences.put(ref, List.of(ai.value().asStringArray()));
}
}

}
}

addToOpenAPIDefinitionProducer
.produce(new AddToOpenAPIDefinitionBuildItem(new SecurityConfigFilter(config, methodReferences)));
}

private boolean isValidOpenAPIMethodForAutoAdd(MethodInfo method, DotName securityRequirement) {
return isOpenAPIEndpoint(method) && !method.hasAnnotation(securityRequirement)
&& method.declaringClass().classAnnotation(securityRequirement) == null;
}

@BuildStep
public List<AllowedJaxRsAnnotationPrefixBuildItem> registerJaxRsSupportedAnnotation() {
List<AllowedJaxRsAnnotationPrefixBuildItem> prefixes = new ArrayList<>();
Expand Down Expand Up @@ -262,6 +309,18 @@ public void registerOpenApiSchemaClassesForReflection(BuildProducer<ReflectiveCl
}
}

private boolean isOpenAPIEndpoint(MethodInfo method) {
Set<DotName> httpAnnotations = new HashSet<>();
httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS);
httpAnnotations.addAll(SpringConstants.HTTP_METHODS);
for (DotName httpAnnotation : httpAnnotations) {
if (method.hasAnnotation(httpAnnotation)) {
return true;
}
}
return false;
}

private void registerReflectionForApiResponseSchemaSerialization(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchy,
Collection<AnnotationInstance> apiResponseAnnotationInstances) {
Expand Down Expand Up @@ -414,12 +473,17 @@ private OpenAPI generateStaticModel(SmallRyeOpenApiConfig openApiConfig) throws
if (openApiConfig.ignoreStaticDocument) {
return null;
} else {
Result result = findStaticModel();
if (result != null) {
try (InputStream is = result.inputStream;
OpenApiStaticFile staticFile = new OpenApiStaticFile(is, result.format)) {
return io.smallrye.openapi.runtime.OpenApiProcessor.modelFromStaticFile(staticFile);
List<Result> results = findStaticModels(openApiConfig);
if (!results.isEmpty()) {
OpenAPI mergedStaticModel = new OpenAPIImpl();
for (Result result : results) {
try (InputStream is = result.inputStream;
OpenApiStaticFile staticFile = new OpenApiStaticFile(is, result.format)) {
OpenAPI staticFileModel = io.smallrye.openapi.runtime.OpenApiProcessor.modelFromStaticFile(staticFile);
mergedStaticModel = MergeUtil.mergeObjects(mergedStaticModel, staticFileModel);
}
}
return mergedStaticModel;
}
return null;
}
Expand All @@ -436,7 +500,6 @@ private OpenAPI generateAnnotationModel(IndexView indexView, Capabilities capabi
if (capabilities.isPresent(Capability.RESTEASY)) {
extensions.add(new RESTEasyExtension(indexView));
}
// TODO: add a Quarkus-REST specific extension that knows the Quarkus REST specific annotations as well as the fact that *param annotations aren't necessary

String defaultPath;
if (resteasyJaxrsConfig.isPresent()) {
Expand Down Expand Up @@ -466,34 +529,74 @@ private String[] getScanners(Capabilities capabilities, IndexView index) {
return scanners.toArray(new String[] {});
}

private Result findStaticModel() {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// Check for the file in both META-INF and WEB-INF/classes/META-INF
Format format = Format.YAML;
InputStream inputStream = cl.getResourceAsStream(META_INF_OPENAPI_YAML);
if (inputStream == null) {
inputStream = cl.getResourceAsStream(WEB_INF_CLASSES_META_INF_OPENAPI_YAML);
}
if (inputStream == null) {
inputStream = cl.getResourceAsStream(META_INF_OPENAPI_YML);
}
if (inputStream == null) {
inputStream = cl.getResourceAsStream(WEB_INF_CLASSES_META_INF_OPENAPI_YML);
private List<Result> findStaticModels(SmallRyeOpenApiConfig openApiConfig) {
List<Result> results = new ArrayList<>();

// First check for the file in both META-INF and WEB-INF/classes/META-INF
results = addStaticModelIfExist(results, Format.YAML, META_INF_OPENAPI_YAML);
results = addStaticModelIfExist(results, Format.YAML, WEB_INF_CLASSES_META_INF_OPENAPI_YAML);
results = addStaticModelIfExist(results, Format.YAML, META_INF_OPENAPI_YML);
results = addStaticModelIfExist(results, Format.YAML, WEB_INF_CLASSES_META_INF_OPENAPI_YML);
results = addStaticModelIfExist(results, Format.JSON, META_INF_OPENAPI_JSON);
results = addStaticModelIfExist(results, Format.JSON, WEB_INF_CLASSES_META_INF_OPENAPI_JSON);

// Add any aditional directories if configured
if (openApiConfig.additionalDocsDirectory.isPresent()) {
List<Path> additionalStaticDocuments = openApiConfig.additionalDocsDirectory.get();
for (Path path : additionalStaticDocuments) {
// Scan all yaml and json files
try {
List<String> filesInDir = getResourceFiles(path.toString());
for (String possibleModelFile : filesInDir) {
results = addStaticModelIfExist(results, possibleModelFile);
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
if (inputStream == null) {
inputStream = cl.getResourceAsStream(META_INF_OPENAPI_JSON);
format = Format.JSON;

return results;
}

private List<Result> addStaticModelIfExist(List<Result> results, String path) {
if (path.endsWith(".json")) {
// Scan a specific json file
results = addStaticModelIfExist(results, Format.JSON, path);
} else if (path.endsWith(".yaml") || path.endsWith(".yml")) {
// Scan a specific yaml file
results = addStaticModelIfExist(results, Format.YAML, path);
}
if (inputStream == null) {
inputStream = cl.getResourceAsStream(WEB_INF_CLASSES_META_INF_OPENAPI_JSON);
format = Format.JSON;
return results;
}

private List<Result> addStaticModelIfExist(List<Result> results, Format format, String path) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try (InputStream inputStream = cl.getResourceAsStream(path)) {
if (inputStream != null) {
results.add(new Result(format, inputStream));
}
} catch (IOException ex) {
ex.printStackTrace();
}
return results;
}

if (inputStream == null) {
return null;
private List<String> getResourceFiles(String path) throws IOException {
List<String> filenames = new ArrayList<>();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try (InputStream inputStream = cl.getResourceAsStream(path)) {
if (inputStream != null) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
String resource;
while ((resource = br.readLine()) != null) {
filenames.add(path + "/" + resource);
}
}
}
}

return new Result(format, inputStream);
return filenames;
}

static class Result {
Expand All @@ -509,6 +612,14 @@ static class Result {
private OpenApiDocument loadDocument(OpenAPI staticModel, OpenAPI annotationModel,
List<AddToOpenAPIDefinitionBuildItem> openAPIBuildItems) {
OpenApiDocument document = prepareOpenApiDocument(staticModel, annotationModel, openAPIBuildItems);

Config c = ConfigProvider.getConfig();
String title = c.getOptionalValue("quarkus.application.name", String.class).orElse("Generated");
String version = c.getOptionalValue("quarkus.application.version", String.class).orElse("1.0");

document.archiveName(title);
document.version(version);

document.initialize();
return document;
}
Expand Down
Loading

0 comments on commit 95bcc1e

Please sign in to comment.