From 0c069471dbc593466eb2e757af2ebe5cfeb47630 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Wed, 27 Sep 2023 09:42:42 +1000 Subject: [PATCH] Allow Build time OpenAPI Filters Signed-off-by: Phillip Kruger --- bom/application/pom.xml | 2 +- .../deployment/SmallRyeOpenApiProcessor.java | 64 +++++++++++++++++-- .../openapi/test/jaxrs/MyBuildTimeFilter.java | 34 ++++++++++ .../openapi/test/jaxrs/MyRunTimeFilter.java | 23 +++++++ .../jaxrs/OpenApiBuiltTimeFilterTestCase.java | 28 ++++++++ .../jaxrs/OpenApiRunTimeFilterTestCase.java | 28 ++++++++ .../smallrye/openapi/OpenApiFilter.java | 27 ++++++++ .../runtime/OpenApiDocumentService.java | 34 +++++++--- .../openapi/runtime/OpenApiRecorder.java | 19 ++++++ 9 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyBuildTimeFilter.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyRunTimeFilter.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiBuiltTimeFilterTestCase.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiRunTimeFilterTestCase.java create mode 100644 extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 83a4d773d4754..fce72e5f859fc 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -54,7 +54,7 @@ 3.3.4 4.0.4 4.0.0 - 3.5.2 + 3.6.0 2.4.0 3.0.3 6.2.6 diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 59504bc294d33..fceacd28f3e58 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -84,6 +84,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.security.Authenticated; +import io.quarkus.smallrye.openapi.OpenApiFilter; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter; @@ -234,6 +235,23 @@ void registerAutoSecurityFilter(BuildProducer syntheticB .supplier(recorder.autoSecurityFilterSupplier(autoSecurityFilter)).done()); } + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer syntheticBeans, + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, + OpenApiRecorder recorder) { + Config config = ConfigProvider.getConfig(); + OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); + + List userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig, + apiFilteredIndexViewBuildItem.getIndex()); + + syntheticBeans.produce(SyntheticBeanBuildItem.configure(OpenApiRecorder.UserDefinedRuntimeFilters.class) + .supplier(recorder.createUserDefinedRuntimeFilters(userDefinedRuntimeFilters)) + .done()); + + } + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) void handler(LaunchModeBuildItem launch, @@ -432,6 +450,37 @@ void addAutoFilters(BuildProducer addToOpenAPID } } + private List getUserDefinedBuildtimeFilters(OpenApiConfig openApiConfig, IndexView index) { + return getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.BUILD); + } + + private List getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, IndexView index) { + List userDefinedFilters = getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.RUN); + // Also add the MP way + String filter = openApiConfig.filter(); + if (filter != null) { + userDefinedFilters.add(filter); + } + return userDefinedFilters; + } + + private List getUserDefinedFilters(OpenApiConfig openApiConfig, IndexView index, OpenApiFilter.RunStage stage) { + List userDefinedFilters = new ArrayList<>(); + Collection annotations = index.getAnnotations(DotName.createSimple(OpenApiFilter.class.getName())); + for (AnnotationInstance ai : annotations) { + AnnotationTarget annotationTarget = ai.target(); + ClassInfo classInfo = annotationTarget.asClass(); + if (classInfo.interfaceNames().contains(DotName.createSimple(OASFilter.class.getName()))) { + + OpenApiFilter.RunStage runStage = OpenApiFilter.RunStage.valueOf(ai.value().asEnum()); + if (runStage.equals(OpenApiFilter.RunStage.BOTH) || runStage.equals(stage)) { + userDefinedFilters.add(classInfo.name().toString()); + } + } + } + return userDefinedFilters; + } + private boolean isManagement(ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, SmallRyeOpenApiConfig smallRyeOpenApiConfig, LaunchModeBuildItem launchModeBuildItem) { @@ -1100,7 +1149,10 @@ private OpenApiDocument storeDocument(OutputTargetBuildItem out, OpenApiDocument document = prepareOpenApiDocument(loadedModel, null, Collections.emptyList(), index); if (includeRuntimeFilters) { - document.filter(filter(openApiConfig, index)); // This usually happens at runtime, so when storing we want to filter here too. + List userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig, index); + for (String s : userDefinedRuntimeFilters) { + document.filter(filter(s, index)); // This usually happens at runtime, so when storing we want to filter here too. + } } // By default, also add the auto generated server @@ -1151,6 +1203,11 @@ private OpenApiDocument prepareOpenApiDocument(OpenAPI staticModel, OASFilter otherExtensionFilter = openAPIBuildItem.getOASFilter(); document.filter(otherExtensionFilter); } + // Add user defined Build time filters + List userDefinedFilters = getUserDefinedBuildtimeFilters(openApiConfig, index); + for (String filter : userDefinedFilters) { + document.filter(filter(filter, index)); + } return document; } @@ -1161,8 +1218,7 @@ private OpenApiDocument createDocument(OpenApiConfig openApiConfig) { return document; } - private OASFilter filter(OpenApiConfig openApiConfig, IndexView index) { - return OpenApiProcessor.getFilter(openApiConfig, - Thread.currentThread().getContextClassLoader(), index); + private OASFilter filter(String className, IndexView index) { + return OpenApiProcessor.getFilter(className, Thread.currentThread().getContextClassLoader(), index); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyBuildTimeFilter.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyBuildTimeFilter.java new file mode 100644 index 0000000000000..49964a83205a4 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyBuildTimeFilter.java @@ -0,0 +1,34 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import java.util.Collection; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.info.Info; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.IndexView; + +import io.quarkus.smallrye.openapi.OpenApiFilter; + +/** + * Filter to add custom elements + */ +@OpenApiFilter(OpenApiFilter.RunStage.BUILD) +public class MyBuildTimeFilter implements OASFilter { + + private IndexView view; + + public MyBuildTimeFilter(IndexView view) { + this.view = view; + } + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + Collection knownClasses = this.view.getKnownClasses(); + Info info = OASFactory.createInfo(); + info.setDescription("Created from Annotated Buildtime filter with " + knownClasses.size() + " known indexed classes"); + openAPI.setInfo(info); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyRunTimeFilter.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyRunTimeFilter.java new file mode 100644 index 0000000000000..5173be5b8f7b4 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/MyRunTimeFilter.java @@ -0,0 +1,23 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.info.Info; + +import io.quarkus.smallrye.openapi.OpenApiFilter; + +/** + * Filter to add custom elements + */ +@OpenApiFilter(OpenApiFilter.RunStage.RUN) +public class MyRunTimeFilter implements OASFilter { + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + Info info = OASFactory.createInfo(); + info.setDescription("Created from Annotated Runtime filter"); + openAPI.setInfo(info); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiBuiltTimeFilterTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiBuiltTimeFilterTestCase.java new file mode 100644 index 0000000000000..f41a305d4c2b2 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiBuiltTimeFilterTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenApiBuiltTimeFilterTestCase { + private static final String OPEN_API_PATH = "/q/openapi"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(OpenApiResource.class, ResourceBean.class, MyBuildTimeFilter.class)); + + @Test + public void testOpenApiFilterResource() { + RestAssured.given().header("Accept", "application/json") + .when().get(OPEN_API_PATH) + .then() + .header("Content-Type", "application/json;charset=UTF-8") + .body("info.description", Matchers.startsWith("Created from Annotated Buildtime filter with")); + + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiRunTimeFilterTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiRunTimeFilterTestCase.java new file mode 100644 index 0000000000000..429287a121567 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiRunTimeFilterTestCase.java @@ -0,0 +1,28 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class OpenApiRunTimeFilterTestCase { + private static final String OPEN_API_PATH = "/q/openapi"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(OpenApiResource.class, ResourceBean.class, MyRunTimeFilter.class)); + + @Test + public void testOpenApiFilterResource() { + RestAssured.given().header("Accept", "application/json") + .when().get(OPEN_API_PATH) + .then() + .header("Content-Type", "application/json;charset=UTF-8") + .body("info.description", Matchers.startsWith("Created from Annotated Runtime filter")); + + } + +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java new file mode 100644 index 0000000000000..f780f70ceff45 --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.openapi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This extends the MP way to define an `org.eclipse.microprofile.openapi.OASFilter`. + * Currently in MP, this needs to be added to a config `mp.openapi.filter` and only allows one filter (class) per application. + * + * This Annotation, that is Quarkus specific, will allow users to annotate one or more classes and that will be + * all that is needed to include the filter. (No config needed). Filters still need to extend. + * + * @see https://download.eclipse.org/microprofile/microprofile-open-api-3.1.1/microprofile-openapi-spec-3.1.1.html#_oasfilter + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface OpenApiFilter { + RunStage value() default RunStage.RUN; // When this filter should run, default Runtime + + static enum RunStage { + BUILD, + RUN, + BOTH + } +} \ No newline at end of file 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 cf5ec42272cf7..a1e10548d3fac 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 @@ -3,6 +3,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; @@ -34,8 +36,8 @@ public class OpenApiDocumentService implements OpenApiDocumentHolder { private final OpenApiDocumentHolder documentHolder; private final String previousOpenApiServersSystemPropertyValue; - public OpenApiDocumentService(OASFilter autoSecurityFilter, Config config) { - + public OpenApiDocumentService(OASFilter autoSecurityFilter, + OpenApiRecorder.UserDefinedRuntimeFilters userDefinedRuntimeFilters, Config config) { String servers = config.getOptionalValue("quarkus.smallrye-openapi.servers", String.class).orElse(null); this.previousOpenApiServersSystemPropertyValue = System.getProperty(OPENAPI_SERVERS); if (servers != null && !servers.isEmpty()) { @@ -43,9 +45,9 @@ public OpenApiDocumentService(OASFilter autoSecurityFilter, Config config) { } if (config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class).orElse(Boolean.FALSE)) { - this.documentHolder = new DynamicDocument(config, autoSecurityFilter); + this.documentHolder = new DynamicDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters()); } else { - this.documentHolder = new StaticDocument(config, autoSecurityFilter); + this.documentHolder = new StaticDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters()); } } @@ -76,7 +78,7 @@ static class StaticDocument implements OpenApiDocumentHolder { private byte[] jsonDocument; private byte[] yamlDocument; - StaticDocument(Config config, OASFilter autoFilter) { + StaticDocument(Config config, OASFilter autoFilter, List userFilters) { ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() : OpenApiConstants.classLoader; try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { @@ -93,7 +95,9 @@ static class StaticDocument implements OpenApiDocumentHolder { document.filter(autoFilter); } document.filter(new DisabledRestEndpointsFilter()); - document.filter(OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX)); + for (String userFilter : userFilters) { + document.filter(OpenApiProcessor.getFilter(userFilter, cl, EMPTY_INDEX)); + } document.initialize(); this.jsonDocument = OpenApiSerializer.serialize(document.get(), Format.JSON) @@ -125,18 +129,26 @@ static class DynamicDocument implements OpenApiDocumentHolder { private OpenAPI generatedOnBuild; private OpenApiConfig openApiConfig; - private OASFilter userFilter; + private List userFilters = new ArrayList<>(); private OASFilter autoFilter; private DisabledRestEndpointsFilter disabledEndpointsFilter; - DynamicDocument(Config config, OASFilter autoFilter) { + DynamicDocument(Config config, OASFilter autoFilter, List annotatedUserFilters) { ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() : OpenApiConstants.classLoader; try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { if (is != null) { try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) { this.openApiConfig = new OpenApiConfigImpl(config); - this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX); + OASFilter microProfileDefinedFilter = OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX); + if (microProfileDefinedFilter != null) { + userFilters.add(microProfileDefinedFilter); + } + for (String annotatedUserFilter : annotatedUserFilters) { + OASFilter annotatedUserDefinedFilter = OpenApiProcessor.getFilter(annotatedUserFilter, cl, + EMPTY_INDEX); + userFilters.add(annotatedUserDefinedFilter); + } this.autoFilter = autoFilter; this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(this.openApiConfig, staticFile); this.disabledEndpointsFilter = new DisabledRestEndpointsFilter(); @@ -182,7 +194,9 @@ private OpenApiDocument getOpenApiDocument() { document.filter(this.autoFilter); } document.filter(this.disabledEndpointsFilter); - document.filter(this.userFilter); + for (OASFilter userFilter : userFilters) { + document.filter(userFilter); + } document.initialize(); return document; } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java index 41d10839c2aec..a816d46d90d88 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.net.URL; import java.util.Enumeration; +import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; @@ -109,4 +110,22 @@ public OASFilter get() { } }; } + + public Supplier createUserDefinedRuntimeFilters(List filters) { + return new Supplier() { + @Override + public UserDefinedRuntimeFilters get() { + return new UserDefinedRuntimeFilters() { + @Override + public List filters() { + return filters; + } + }; + } + }; + } + + public interface UserDefinedRuntimeFilters { + List filters(); + } }