Skip to content

Commit 05ea722

Browse files
Merge pull request quarkusio#36152 from phillip-kruger/openapi-build-filter
Allow Build time OpenAPI Filters
2 parents c8a911c + 0c06947 commit 05ea722

File tree

9 files changed

+244
-15
lines changed

9 files changed

+244
-15
lines changed

bom/application/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<smallrye-config.version>3.3.4</smallrye-config.version>
5555
<smallrye-health.version>4.0.4</smallrye-health.version>
5656
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
57-
<smallrye-open-api.version>3.5.2</smallrye-open-api.version>
57+
<smallrye-open-api.version>3.6.0</smallrye-open-api.version>
5858
<smallrye-graphql.version>2.4.0</smallrye-graphql.version>
5959
<smallrye-opentracing.version>3.0.3</smallrye-opentracing.version>
6060
<smallrye-fault-tolerance.version>6.2.6</smallrye-fault-tolerance.version>

extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import io.quarkus.runtime.LaunchMode;
8585
import io.quarkus.runtime.util.ClassPathUtils;
8686
import io.quarkus.security.Authenticated;
87+
import io.quarkus.smallrye.openapi.OpenApiFilter;
8788
import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig;
8889
import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter;
8990
import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter;
@@ -234,6 +235,23 @@ void registerAutoSecurityFilter(BuildProducer<SyntheticBeanBuildItem> syntheticB
234235
.supplier(recorder.autoSecurityFilterSupplier(autoSecurityFilter)).done());
235236
}
236237

238+
@BuildStep
239+
@Record(ExecutionTime.STATIC_INIT)
240+
void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
241+
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
242+
OpenApiRecorder recorder) {
243+
Config config = ConfigProvider.getConfig();
244+
OpenApiConfig openApiConfig = new OpenApiConfigImpl(config);
245+
246+
List<String> userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig,
247+
apiFilteredIndexViewBuildItem.getIndex());
248+
249+
syntheticBeans.produce(SyntheticBeanBuildItem.configure(OpenApiRecorder.UserDefinedRuntimeFilters.class)
250+
.supplier(recorder.createUserDefinedRuntimeFilters(userDefinedRuntimeFilters))
251+
.done());
252+
253+
}
254+
237255
@BuildStep
238256
@Record(ExecutionTime.RUNTIME_INIT)
239257
void handler(LaunchModeBuildItem launch,
@@ -432,6 +450,37 @@ void addAutoFilters(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPID
432450
}
433451
}
434452

453+
private List<String> getUserDefinedBuildtimeFilters(OpenApiConfig openApiConfig, IndexView index) {
454+
return getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.BUILD);
455+
}
456+
457+
private List<String> getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, IndexView index) {
458+
List<String> userDefinedFilters = getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.RUN);
459+
// Also add the MP way
460+
String filter = openApiConfig.filter();
461+
if (filter != null) {
462+
userDefinedFilters.add(filter);
463+
}
464+
return userDefinedFilters;
465+
}
466+
467+
private List<String> getUserDefinedFilters(OpenApiConfig openApiConfig, IndexView index, OpenApiFilter.RunStage stage) {
468+
List<String> userDefinedFilters = new ArrayList<>();
469+
Collection<AnnotationInstance> annotations = index.getAnnotations(DotName.createSimple(OpenApiFilter.class.getName()));
470+
for (AnnotationInstance ai : annotations) {
471+
AnnotationTarget annotationTarget = ai.target();
472+
ClassInfo classInfo = annotationTarget.asClass();
473+
if (classInfo.interfaceNames().contains(DotName.createSimple(OASFilter.class.getName()))) {
474+
475+
OpenApiFilter.RunStage runStage = OpenApiFilter.RunStage.valueOf(ai.value().asEnum());
476+
if (runStage.equals(OpenApiFilter.RunStage.BOTH) || runStage.equals(stage)) {
477+
userDefinedFilters.add(classInfo.name().toString());
478+
}
479+
}
480+
}
481+
return userDefinedFilters;
482+
}
483+
435484
private boolean isManagement(ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig,
436485
SmallRyeOpenApiConfig smallRyeOpenApiConfig,
437486
LaunchModeBuildItem launchModeBuildItem) {
@@ -1100,7 +1149,10 @@ private OpenApiDocument storeDocument(OutputTargetBuildItem out,
11001149
OpenApiDocument document = prepareOpenApiDocument(loadedModel, null, Collections.emptyList(), index);
11011150

11021151
if (includeRuntimeFilters) {
1103-
document.filter(filter(openApiConfig, index)); // This usually happens at runtime, so when storing we want to filter here too.
1152+
List<String> userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig, index);
1153+
for (String s : userDefinedRuntimeFilters) {
1154+
document.filter(filter(s, index)); // This usually happens at runtime, so when storing we want to filter here too.
1155+
}
11041156
}
11051157

11061158
// By default, also add the auto generated server
@@ -1151,6 +1203,11 @@ private OpenApiDocument prepareOpenApiDocument(OpenAPI staticModel,
11511203
OASFilter otherExtensionFilter = openAPIBuildItem.getOASFilter();
11521204
document.filter(otherExtensionFilter);
11531205
}
1206+
// Add user defined Build time filters
1207+
List<String> userDefinedFilters = getUserDefinedBuildtimeFilters(openApiConfig, index);
1208+
for (String filter : userDefinedFilters) {
1209+
document.filter(filter(filter, index));
1210+
}
11541211
return document;
11551212
}
11561213

@@ -1161,8 +1218,7 @@ private OpenApiDocument createDocument(OpenApiConfig openApiConfig) {
11611218
return document;
11621219
}
11631220

1164-
private OASFilter filter(OpenApiConfig openApiConfig, IndexView index) {
1165-
return OpenApiProcessor.getFilter(openApiConfig,
1166-
Thread.currentThread().getContextClassLoader(), index);
1221+
private OASFilter filter(String className, IndexView index) {
1222+
return OpenApiProcessor.getFilter(className, Thread.currentThread().getContextClassLoader(), index);
11671223
}
11681224
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import java.util.Collection;
4+
5+
import org.eclipse.microprofile.openapi.OASFactory;
6+
import org.eclipse.microprofile.openapi.OASFilter;
7+
import org.eclipse.microprofile.openapi.models.OpenAPI;
8+
import org.eclipse.microprofile.openapi.models.info.Info;
9+
import org.jboss.jandex.ClassInfo;
10+
import org.jboss.jandex.IndexView;
11+
12+
import io.quarkus.smallrye.openapi.OpenApiFilter;
13+
14+
/**
15+
* Filter to add custom elements
16+
*/
17+
@OpenApiFilter(OpenApiFilter.RunStage.BUILD)
18+
public class MyBuildTimeFilter implements OASFilter {
19+
20+
private IndexView view;
21+
22+
public MyBuildTimeFilter(IndexView view) {
23+
this.view = view;
24+
}
25+
26+
@Override
27+
public void filterOpenAPI(OpenAPI openAPI) {
28+
Collection<ClassInfo> knownClasses = this.view.getKnownClasses();
29+
Info info = OASFactory.createInfo();
30+
info.setDescription("Created from Annotated Buildtime filter with " + knownClasses.size() + " known indexed classes");
31+
openAPI.setInfo(info);
32+
}
33+
34+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.eclipse.microprofile.openapi.OASFactory;
4+
import org.eclipse.microprofile.openapi.OASFilter;
5+
import org.eclipse.microprofile.openapi.models.OpenAPI;
6+
import org.eclipse.microprofile.openapi.models.info.Info;
7+
8+
import io.quarkus.smallrye.openapi.OpenApiFilter;
9+
10+
/**
11+
* Filter to add custom elements
12+
*/
13+
@OpenApiFilter(OpenApiFilter.RunStage.RUN)
14+
public class MyRunTimeFilter implements OASFilter {
15+
16+
@Override
17+
public void filterOpenAPI(OpenAPI openAPI) {
18+
Info info = OASFactory.createInfo();
19+
info.setDescription("Created from Annotated Runtime filter");
20+
openAPI.setInfo(info);
21+
}
22+
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.hamcrest.Matchers;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.RegisterExtension;
6+
7+
import io.quarkus.test.QuarkusUnitTest;
8+
import io.restassured.RestAssured;
9+
10+
public class OpenApiBuiltTimeFilterTestCase {
11+
private static final String OPEN_API_PATH = "/q/openapi";
12+
13+
@RegisterExtension
14+
static QuarkusUnitTest runner = new QuarkusUnitTest()
15+
.withApplicationRoot((jar) -> jar
16+
.addClasses(OpenApiResource.class, ResourceBean.class, MyBuildTimeFilter.class));
17+
18+
@Test
19+
public void testOpenApiFilterResource() {
20+
RestAssured.given().header("Accept", "application/json")
21+
.when().get(OPEN_API_PATH)
22+
.then()
23+
.header("Content-Type", "application/json;charset=UTF-8")
24+
.body("info.description", Matchers.startsWith("Created from Annotated Buildtime filter with"));
25+
26+
}
27+
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.quarkus.smallrye.openapi.test.jaxrs;
2+
3+
import org.hamcrest.Matchers;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.RegisterExtension;
6+
7+
import io.quarkus.test.QuarkusUnitTest;
8+
import io.restassured.RestAssured;
9+
10+
public class OpenApiRunTimeFilterTestCase {
11+
private static final String OPEN_API_PATH = "/q/openapi";
12+
13+
@RegisterExtension
14+
static QuarkusUnitTest runner = new QuarkusUnitTest()
15+
.withApplicationRoot((jar) -> jar
16+
.addClasses(OpenApiResource.class, ResourceBean.class, MyRunTimeFilter.class));
17+
18+
@Test
19+
public void testOpenApiFilterResource() {
20+
RestAssured.given().header("Accept", "application/json")
21+
.when().get(OPEN_API_PATH)
22+
.then()
23+
.header("Content-Type", "application/json;charset=UTF-8")
24+
.body("info.description", Matchers.startsWith("Created from Annotated Runtime filter"));
25+
26+
}
27+
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkus.smallrye.openapi;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* This extends the MP way to define an `org.eclipse.microprofile.openapi.OASFilter`.
10+
* Currently in MP, this needs to be added to a config `mp.openapi.filter` and only allows one filter (class) per application.
11+
*
12+
* This Annotation, that is Quarkus specific, will allow users to annotate one or more classes and that will be
13+
* all that is needed to include the filter. (No config needed). Filters still need to extend.
14+
*
15+
* @see https://download.eclipse.org/microprofile/microprofile-open-api-3.1.1/microprofile-openapi-spec-3.1.1.html#_oasfilter
16+
*/
17+
@Retention(RetentionPolicy.RUNTIME)
18+
@Target(ElementType.TYPE)
19+
public @interface OpenApiFilter {
20+
RunStage value() default RunStage.RUN; // When this filter should run, default Runtime
21+
22+
static enum RunStage {
23+
BUILD,
24+
RUN,
25+
BOTH
26+
}
27+
}

extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.io.IOException;
44
import java.io.InputStream;
55
import java.nio.charset.StandardCharsets;
6+
import java.util.ArrayList;
7+
import java.util.List;
68

79
import jakarta.enterprise.context.ApplicationScoped;
810
import jakarta.enterprise.event.Observes;
@@ -34,18 +36,18 @@ public class OpenApiDocumentService implements OpenApiDocumentHolder {
3436
private final OpenApiDocumentHolder documentHolder;
3537
private final String previousOpenApiServersSystemPropertyValue;
3638

37-
public OpenApiDocumentService(OASFilter autoSecurityFilter, Config config) {
38-
39+
public OpenApiDocumentService(OASFilter autoSecurityFilter,
40+
OpenApiRecorder.UserDefinedRuntimeFilters userDefinedRuntimeFilters, Config config) {
3941
String servers = config.getOptionalValue("quarkus.smallrye-openapi.servers", String.class).orElse(null);
4042
this.previousOpenApiServersSystemPropertyValue = System.getProperty(OPENAPI_SERVERS);
4143
if (servers != null && !servers.isEmpty()) {
4244
System.setProperty(OPENAPI_SERVERS, servers);
4345
}
4446

4547
if (config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class).orElse(Boolean.FALSE)) {
46-
this.documentHolder = new DynamicDocument(config, autoSecurityFilter);
48+
this.documentHolder = new DynamicDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters());
4749
} else {
48-
this.documentHolder = new StaticDocument(config, autoSecurityFilter);
50+
this.documentHolder = new StaticDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters());
4951
}
5052
}
5153

@@ -76,7 +78,7 @@ static class StaticDocument implements OpenApiDocumentHolder {
7678
private byte[] jsonDocument;
7779
private byte[] yamlDocument;
7880

79-
StaticDocument(Config config, OASFilter autoFilter) {
81+
StaticDocument(Config config, OASFilter autoFilter, List<String> userFilters) {
8082
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
8183
: OpenApiConstants.classLoader;
8284
try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) {
@@ -93,7 +95,9 @@ static class StaticDocument implements OpenApiDocumentHolder {
9395
document.filter(autoFilter);
9496
}
9597
document.filter(new DisabledRestEndpointsFilter());
96-
document.filter(OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX));
98+
for (String userFilter : userFilters) {
99+
document.filter(OpenApiProcessor.getFilter(userFilter, cl, EMPTY_INDEX));
100+
}
97101
document.initialize();
98102

99103
this.jsonDocument = OpenApiSerializer.serialize(document.get(), Format.JSON)
@@ -125,18 +129,26 @@ static class DynamicDocument implements OpenApiDocumentHolder {
125129

126130
private OpenAPI generatedOnBuild;
127131
private OpenApiConfig openApiConfig;
128-
private OASFilter userFilter;
132+
private List<OASFilter> userFilters = new ArrayList<>();
129133
private OASFilter autoFilter;
130134
private DisabledRestEndpointsFilter disabledEndpointsFilter;
131135

132-
DynamicDocument(Config config, OASFilter autoFilter) {
136+
DynamicDocument(Config config, OASFilter autoFilter, List<String> annotatedUserFilters) {
133137
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
134138
: OpenApiConstants.classLoader;
135139
try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) {
136140
if (is != null) {
137141
try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) {
138142
this.openApiConfig = new OpenApiConfigImpl(config);
139-
this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX);
143+
OASFilter microProfileDefinedFilter = OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX);
144+
if (microProfileDefinedFilter != null) {
145+
userFilters.add(microProfileDefinedFilter);
146+
}
147+
for (String annotatedUserFilter : annotatedUserFilters) {
148+
OASFilter annotatedUserDefinedFilter = OpenApiProcessor.getFilter(annotatedUserFilter, cl,
149+
EMPTY_INDEX);
150+
userFilters.add(annotatedUserDefinedFilter);
151+
}
140152
this.autoFilter = autoFilter;
141153
this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(this.openApiConfig, staticFile);
142154
this.disabledEndpointsFilter = new DisabledRestEndpointsFilter();
@@ -182,7 +194,9 @@ private OpenApiDocument getOpenApiDocument() {
182194
document.filter(this.autoFilter);
183195
}
184196
document.filter(this.disabledEndpointsFilter);
185-
document.filter(this.userFilter);
197+
for (OASFilter userFilter : userFilters) {
198+
document.filter(userFilter);
199+
}
186200
document.initialize();
187201
return document;
188202
}

extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.InputStream;
55
import java.net.URL;
66
import java.util.Enumeration;
7+
import java.util.List;
78
import java.util.function.Consumer;
89
import java.util.function.Supplier;
910

@@ -109,4 +110,22 @@ public OASFilter get() {
109110
}
110111
};
111112
}
113+
114+
public Supplier<?> createUserDefinedRuntimeFilters(List<String> filters) {
115+
return new Supplier<Object>() {
116+
@Override
117+
public UserDefinedRuntimeFilters get() {
118+
return new UserDefinedRuntimeFilters() {
119+
@Override
120+
public List<String> filters() {
121+
return filters;
122+
}
123+
};
124+
}
125+
};
126+
}
127+
128+
public interface UserDefinedRuntimeFilters {
129+
List<String> filters();
130+
}
112131
}

0 commit comments

Comments
 (0)