diff --git a/bom/application/pom.xml b/bom/application/pom.xml index eff27d149108f..988a89550f3aa 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -50,12 +50,12 @@ 3.0.1 2.1 2.0 - 3.1.1 + 3.1.2 2.6.0 3.9.1 4.1.0 4.0.0 - 3.12.0 + 3.13.0 2.10.0 6.4.0 4.6.0 diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenAPIRuntimeBuilder.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenAPIRuntimeBuilder.java new file mode 100644 index 0000000000000..8b85ea0e6b8c7 --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenAPIRuntimeBuilder.java @@ -0,0 +1,20 @@ +package io.quarkus.smallrye.openapi.runtime; + +import io.smallrye.openapi.api.SmallRyeOpenAPI; + +/** + * Customized {@link SmallRyeOpenAPI.Builder} implementation that only includes + * functionality that should occur at application runtime. Specifically, it only + * supports loading a static OpenAPI file/stream, applying OASFilter instances, + * and writing the OpenAPI model to a DOM for later serialization to JSON or + * YAML. + */ +class OpenAPIRuntimeBuilder extends SmallRyeOpenAPI.Builder { + @Override + public SmallRyeOpenAPI build() { + var ctx = super.getContext(); + buildPrepare(ctx); + buildStaticModel(ctx); + return buildFinalize(ctx); + } +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentHolder.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentHolder.java index 45fe3f6496f69..7e240e1f24b38 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentHolder.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentHolder.java @@ -1,7 +1,5 @@ package io.quarkus.smallrye.openapi.runtime; -import io.smallrye.openapi.runtime.io.Format; - /** * Holds instances of the OpenAPI Document */ @@ -11,10 +9,4 @@ public interface OpenApiDocumentHolder { public byte[] getYamlDocument(); - default byte[] getDocument(Format format) { - if (format.equals(Format.JSON)) { - return getJsonDocument(); - } - return getYamlDocument(); - } } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java index 8d9676a3c0171..3999e2a059589 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java @@ -4,45 +4,63 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -import java.util.List; +import java.util.LinkedHashSet; import java.util.Optional; +import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.openapi.OASConfig; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.IndexView; import io.quarkus.smallrye.openapi.runtime.filter.DisabledRestEndpointsFilter; import io.smallrye.openapi.api.SmallRyeOpenAPI; +import io.smallrye.openapi.runtime.io.Format; /** * Loads the document and make it available */ @ApplicationScoped -public class OpenApiDocumentService implements OpenApiDocumentHolder { +public class OpenApiDocumentService { private final OpenApiDocumentHolder documentHolder; @Inject public OpenApiDocumentService(OASFilter autoSecurityFilter, - OpenApiRecorder.UserDefinedRuntimeFilters userDefinedRuntimeFilters, Config config) { + OpenApiRecorder.UserDefinedRuntimeFilters runtimeFilters, Config config) { ClassLoader loader = Optional.ofNullable(OpenApiConstants.classLoader) .orElseGet(Thread.currentThread()::getContextClassLoader); try (InputStream source = loader.getResourceAsStream(OpenApiConstants.BASE_NAME + "JSON")) { if (source != null) { - var userFilters = userDefinedRuntimeFilters.filters(); + Set userFilters = new LinkedHashSet<>(runtimeFilters.filters()); boolean dynamic = config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class) .orElse(Boolean.FALSE); - - if (dynamic) { - this.documentHolder = new DynamicDocument(source, config, autoSecurityFilter, userFilters); + SmallRyeOpenAPI.Builder builder = new OpenAPIRuntimeBuilder() + .withConfig(config) + .withApplicationClassLoader(loader) + .enableModelReader(false) + .enableStandardStaticFiles(false) + .enableAnnotationScan(false) + .enableStandardFilter(false) + .withCustomStaticFile(() -> source); + + // Auth-security and disabled endpoint filters will only run once + Optional.ofNullable(autoSecurityFilter) + .ifPresent(builder::addFilter); + DisabledRestEndpointsFilter.maybeGetInstance() + .ifPresent(builder::addFilter); + + if (dynamic && !userFilters.isEmpty()) { + // Only regenerate the OpenAPI document when configured and there are filters to run + this.documentHolder = new DynamicDocument(builder, loader, userFilters); } else { - this.documentHolder = new StaticDocument(source, config, autoSecurityFilter, userFilters); + userFilters.forEach(name -> builder.addFilter(name, loader, (IndexView) null)); + this.documentHolder = new StaticDocument(builder.build()); } } else { this.documentHolder = new EmptyDocument(); @@ -52,12 +70,11 @@ public OpenApiDocumentService(OASFilter autoSecurityFilter, } } - public byte[] getJsonDocument() { - return this.documentHolder.getJsonDocument(); - } - - public byte[] getYamlDocument() { - return this.documentHolder.getYamlDocument(); + byte[] getDocument(Format format) { + if (format.equals(Format.JSON)) { + return documentHolder.getJsonDocument(); + } + return documentHolder.getYamlDocument(); } static class EmptyDocument implements OpenApiDocumentHolder { @@ -80,20 +97,7 @@ static class StaticDocument implements OpenApiDocumentHolder { private byte[] jsonDocument; private byte[] yamlDocument; - StaticDocument(InputStream source, Config config, OASFilter autoFilter, List userFilters) { - SmallRyeOpenAPI.Builder builder = SmallRyeOpenAPI.builder() - .withConfig(config) - .enableModelReader(false) - .enableStandardStaticFiles(false) - .enableAnnotationScan(false) - .enableStandardFilter(false) - .withCustomStaticFile(() -> source); - - Optional.ofNullable(autoFilter).ifPresent(builder::addFilter); - builder.addFilter(new DisabledRestEndpointsFilter()); - userFilters.forEach(builder::addFilterName); - - SmallRyeOpenAPI openAPI = builder.build(); + StaticDocument(SmallRyeOpenAPI openAPI) { jsonDocument = openAPI.toJSON().getBytes(StandardCharsets.UTF_8); yamlDocument = openAPI.toYAML().getBytes(StandardCharsets.UTF_8); } @@ -108,31 +112,18 @@ public byte[] getYamlDocument() { } /** - * Generate the document on every request. + * Generate the document on every request by re-running user-provided OASFilters. */ static class DynamicDocument implements OpenApiDocumentHolder { private SmallRyeOpenAPI.Builder builder; - private OpenAPI generatedOnBuild; - - DynamicDocument(InputStream source, Config config, OASFilter autoFilter, List annotatedUserFilters) { - builder = SmallRyeOpenAPI.builder() - .withConfig(config) - .enableModelReader(false) - .enableStandardStaticFiles(false) - .enableAnnotationScan(false) - .enableStandardFilter(false) - .withCustomStaticFile(() -> source); - - generatedOnBuild = builder.build().model(); + DynamicDocument(SmallRyeOpenAPI.Builder builder, ClassLoader loader, Set userFilters) { + OpenAPI generatedOnBuild = builder.build().model(); builder.withCustomStaticFile(() -> null); builder.withInitialModel(generatedOnBuild); - - Optional.ofNullable(autoFilter).ifPresent(builder::addFilter); - builder.addFilter(new DisabledRestEndpointsFilter()); - config.getOptionalValue(OASConfig.FILTER, String.class).ifPresent(builder::addFilterName); - annotatedUserFilters.forEach(builder::addFilterName); + userFilters.forEach(name -> builder.addFilter(name, loader, (IndexView) null)); + this.builder = builder; } public byte[] getJsonDocument() { diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/DisabledRestEndpointsFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/DisabledRestEndpointsFilter.java index f5da1e8fb106f..0a98f3024eb52 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/DisabledRestEndpointsFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/DisabledRestEndpointsFilter.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; @@ -20,11 +19,29 @@ */ public class DisabledRestEndpointsFilter implements OASFilter { + public static Optional maybeGetInstance() { + var endpoints = DisabledRestEndpoints.get(); + + if (endpoints != null && !endpoints.isEmpty()) { + return Optional.of(new DisabledRestEndpointsFilter(endpoints)); + } + + return Optional.empty(); + } + + final Map> disabledEndpoints; + + private DisabledRestEndpointsFilter(Map> disabledEndpoints) { + this.disabledEndpoints = disabledEndpoints; + } + @Override public void filterOpenAPI(OpenAPI openAPI) { Paths paths = openAPI.getPaths(); - disabledRestEndpoints() + disabledEndpoints.entrySet() + .stream() + .map(pathMethods -> Map.entry(stripSlash(pathMethods.getKey()), pathMethods.getValue())) // Skip paths that are not present in the OpenAPI model .filter(pathMethods -> paths.hasPathItem(pathMethods.getKey())) .forEach(pathMethods -> { @@ -44,14 +61,6 @@ public void filterOpenAPI(OpenAPI openAPI) { }); } - static Stream>> disabledRestEndpoints() { - return Optional.ofNullable(DisabledRestEndpoints.get()) - .orElseGet(Collections::emptyMap) - .entrySet() - .stream() - .map(pathMethods -> Map.entry(stripSlash(pathMethods.getKey()), pathMethods.getValue())); - } - /** * Removes any trailing slash character from the path when it is not the root '/' * path. This is necessary to align with the paths generated in the OpenAPI model.