Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions docs/src/main/asciidoc/openapi-swaggerui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,12 @@ You can change the Generated OpenAPI Schema using one or more filter. Filters ca
/**
* Filter to add custom elements
*/
@OpenApiFilter(OpenApiFilter.RunStage.BUILD) //<1>
public class MyBuildTimeFilter implements OASFilter { //<2>
@OpenApiFilter(stages = {OpenApiFilter.RunStage.BUILD, OpenApiFilter.RunStage.RUNTIME_PER_REQUEST}) //<1>
public class MyMultiStageFilter implements OASFilter { //<2>

private IndexView view;

public MyBuildTimeFilter(IndexView view) { //<3>
public MyMultiStageFilter(IndexView view) { //<3>
this.view = view;
}

Expand All @@ -350,17 +350,54 @@ public class MyBuildTimeFilter implements OASFilter { //<2>

}
----
<1> Annotate method with `@OpenApiFilter` and the run stage (BUILD,RUN,BOTH)
<2> Implement OASFilter and override any of the methods
<3> For Build stage filters, the index can be passed in (optional)
<1> Annotate the class with `@OpenApiFilter` and use the `stages` field with one or more of `BUILD`, `RUNTIME_STARTUP`, or `RUNTIME_PER_REQUEST`. This filter runs once during build, and once every time the OpenAPI document is requested.
<2> Implement OASFilter and override any of its methods
<3> Filters which run during the build of the application - i.e. `BUILD` Stage filters (See also <<Advanced interactions of RunStages>>) - the Jandex index can be passed in the constructor. During application runtime, the Jandex index will *not* be null. However it will be empty, i.e it won't contain info about any classes or annotations.
<4> Get a hold of the generated `OpenAPI` Schema, and enhance as required

Remember that setting fields on the schema will override what has been generated, you might want to get and add to (so modify). Also know that the generated values might be null, so you will have to check for that.

=== Runtime filters
=== Overview of RunStages

Runtime filters by default runs on startup (when the final OpenAPI document gets created). You can change runtime filters to run on every request, making the OpenAPI document dynamic.
To do this you need to set this propery: `quarkus.smallrye-openapi.always-run-filter=true`.
[cols="1,3", options="header"]
|===
| Stage | Meaning

| BUILD
| The filter executes at build time.

| RUNTIME_STARTUP
| The filter executes (eagerly) at application startup.

| RUNTIME_PER_REQUEST
| The filter executes each time the OpenAPI document is requested.

| RUN
| DEPRECATED. The filter executes at stage `RUNTIME_STARTUP` if `quarkus.smallrye-openapi.always-run-filter` is set to `false`,
or at stage `RUNTIME_PER_REQUEST` if `quarkus.smallrye-openapi.always-run-filter` is set to `true`. (See also <<Advanced interactions of RunStages>>.)

| BOTH
| DEPRECATED. The filter executes as if both the `BUILD` and `RUN` stages were specified.
|===

NOTE: The `quarkus.smallrye-openapi.always-run-filter` configuration property is deprecated. Annotate your filters with `@OpenApiFilter(stages = OpenApiFilter.RunStage.RUNTIME_PER_REQUEST)` instead.


=== Advanced interactions of RunStages

While the `BUILD` RunStage is expected to run at build time - as the name already suggests - there are additional RunStages that also execute at build time.

The `quarkus-smallrye-openapi` extension saves the OpenAPI Document to a file when the `quarkus.smallrye-openapi.store-schema-directory` is configured.
This saved version of the OpenAPI Document is useful for downstream consumers.

To produce an OpenAPI Document which possibly *could* match the one produced during runtime, the `RUN` and `RUNTIME_STARTUP` RunStages are executed at build time - in a separate step from the `BUILD` RunStage.
Any exception which occurs during this additional processing step is ignored, i.e. if one of the filters throws, none will have any effect.

This additional processing step is done even if `quarkus.smallrye-openapi.store-schema-directory` is *NOT* configured.

=== The MicroProfile OpenAPI way

The Microprofile OpenAPI specification defines that *one* OASFilter implementation can be specified using the `mp.openapi.filter` configuration property. This filter will be run during RunStage `RUN`.

== Loading OpenAPI Schema From Static Files

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ public interface OpenApiDocumentConfig {

/**
* Do not run the filter only at startup, but every time the document is requested (dynamic).
*
* @deprecated Use {@code @OpenApiFilter(stages = RunStage.RUNTIME_PER_REQUEST)} instead to mark
* individual filters as per-request. This makes the OpenAPI document dynamic
* automatically without requiring this configuration property.
*/
@Deprecated(since = "3.34", forRemoval = true)
@WithDefault("false")
boolean alwaysRunFilter();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -235,7 +236,7 @@ void prepareDocuments(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,

openApiConfig.documents().forEach((documentName, documentConfig) -> {

List<String> userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(config,
Map<OpenApiFilter.RunStage, List<String>> filtersByStage = getUserDefinedFiltersByStage(config,
apiFilteredIndexViewBuildItem.getIndex(), documentName);

AutoSecurityFilter autoSecurityFilter = null;
Expand All @@ -246,9 +247,15 @@ void prepareDocuments(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
.orElse(null);
}

recorder.prepareDocument(autoSecurityFilter, userDefinedRuntimeFilters, documentName);
recorder.prepareDocument(autoSecurityFilter, filtersByStage, documentName);

reflectiveClass.produce(ReflectiveClassBuildItem.builder(userDefinedRuntimeFilters.toArray(new String[] {}))
List<String> allRuntimeFilters = new ArrayList<>();
filtersByStage.forEach((stage, filters) -> {
if (stage != OpenApiFilter.RunStage.BUILD) {
allRuntimeFilters.addAll(filters);
}
});
reflectiveClass.produce(ReflectiveClassBuildItem.builder(allRuntimeFilters.toArray(new String[] {}))
.reason(getClass().getName()).build());
});
}
Expand Down Expand Up @@ -277,7 +284,9 @@ void registerAnnotatedUserDefinedRuntimeFilters(

for (String documentName : documentNames) {
Config wrappedConfig = OpenApiConfigHelper.wrap(config, documentName);
userDefinedRuntimeFilters.addAll(getUserDefinedRuntimeFilters(wrappedConfig, index, documentName));
userDefinedRuntimeFilters.addAll(getUserDefinedRuntimeStartupFilters(wrappedConfig, index, documentName));
userDefinedRuntimeFilters
.addAll(getUserDefinedFilters(index, documentName, OpenApiFilter.RunStage.RUNTIME_PER_REQUEST));
}
}

Expand All @@ -290,6 +299,22 @@ void registerAnnotatedUserDefinedRuntimeFilters(
unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(runtimeFilterClassNames));
}

@BuildStep
@Produce(ServiceStartBuildItem.class)
void validateOpenApiFilterStages(BeanArchiveIndexBuildItem indexBuildItem) {
IndexView index = indexBuildItem.getIndex();
Collection<AnnotationInstance> annotations = index.getAnnotations(NAME_OPEN_API_FILTER);

for (AnnotationInstance annotation : annotations) {
AnnotationValue stagesValue = annotation.valueWithDefault(index, "stages");
if (stagesValue.asArrayList().isEmpty()) {
log.warnf(
"@OpenApiFilter on '%s' will not be run, since the stages array is set to an empty array (stages = {}).",
annotation.target().asClass().name());
}
}
}

@BuildStep
@Produce(ServiceStartBuildItem.class)
void validateOpenApiFilterDocumentNames(SmallRyeOpenApiConfig config,
Expand Down Expand Up @@ -356,6 +381,7 @@ void handler(LaunchModeBuildItem launch,
BuildProducer<SystemPropertyBuildItem> systemProperties,
OpenApiRecorder recorder,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
ShutdownContextBuildItem shutdownContext,
SmallRyeOpenApiConfig openApiConfig,
List<FilterBuildItem> filterBuildItems,
Expand Down Expand Up @@ -392,9 +418,12 @@ void handler(LaunchModeBuildItem launch,
String documentName = entry.getKey();
OpenApiDocumentConfig documentConfig = entry.getValue();

Handler<RoutingContext> handler = recorder.handler(documentName, openApiConfig.documents()
.get(documentName)
.alwaysRunFilter());
boolean hasPerRequestFilters = !getUserDefinedFilters(
apiFilteredIndexViewBuildItem.getIndex(), documentName, OpenApiFilter.RunStage.RUNTIME_PER_REQUEST)
.isEmpty();

boolean dynamic = documentConfig.alwaysRunFilter() || hasPerRequestFilters;
Handler<RoutingContext> handler = recorder.handler(documentName, dynamic);

String managementEnabledKey = MANAGEMENT_ENABLED;

Expand Down Expand Up @@ -578,27 +607,98 @@ void addAutoFilters(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPID
});
}

private List<String> getUserDefinedBuildTimeFilters(IndexView index, String documentName) {
return getUserDefinedFilters(index, OpenApiFilter.RunStage.BUILD, documentName);
}

private List<String> getUserDefinedRuntimeFilters(Config config, IndexView index, String documentName) {
List<String> userDefinedFilters = getUserDefinedFilters(index, OpenApiFilter.RunStage.RUN, documentName);
private List<String> getUserDefinedRuntimeStartupFilters(Config config, IndexView index, String documentName) {
@SuppressWarnings("removal")
List<String> userDefinedFilters = getUserDefinedFilters(index,
documentName, OpenApiFilter.RunStage.RUNTIME_STARTUP, OpenApiFilter.RunStage.RUN);
// Also add the MP way
config.getOptionalValue(OASConfig.FILTER, String.class).ifPresent(userDefinedFilters::add);
return userDefinedFilters;
}

private List<String> getUserDefinedFilters(IndexView index, OpenApiFilter.RunStage stage, String documentName) {
EnumSet<OpenApiFilter.RunStage> stages = EnumSet.of(OpenApiFilter.RunStage.BOTH, stage);
/**
* Builds a map of all user-defined filters grouped by their resolved {@link OpenApiFilter.RunStage}.
* The map never contains {@link OpenApiFilter.RunStage#BOTH} as a key; filters annotated with
* {@code BOTH} are resolved to {@code BUILD} + {@code RUN}.
*/
@SuppressWarnings("removal")
private Map<OpenApiFilter.RunStage, List<String>> getUserDefinedFiltersByStage(Config config, IndexView index,
String documentName) {
Map<OpenApiFilter.RunStage, List<String>> result = new EnumMap<>(OpenApiFilter.RunStage.class);
for (OpenApiFilter.RunStage stage : OpenApiFilter.RunStage.values()) {
if (stage == OpenApiFilter.RunStage.BOTH) {
continue;
}
result.put(stage, getUserDefinedFilters(index, documentName, stage));
}

// Also add the MP way
config.getOptionalValue(OASConfig.FILTER, String.class)
.ifPresent(filter -> result.get(OpenApiFilter.RunStage.RUN).add(filter));
return result;
}

/**
* resolves the effective stages from {@link OpenApiFilter#stages()} and {@link OpenApiFilter#value()}.
*
* @param ai the OpenApiFilter annotation placed on an OASFilter implementation
* @param index
* @return set of the Runstages this OasFilter should run in, never null.
* {@link io.quarkus.smallrye.openapi.OpenApiFilter.RunStage#BOTH} will not be present, instead it will be resolved
* to {@link io.quarkus.smallrye.openapi.OpenApiFilter.RunStage#BUILD} +
* {@link io.quarkus.smallrye.openapi.OpenApiFilter.RunStage#RUN}
* @deprecated This will be removed once {@link OpenApiFilter#value()} is also removed.
*/
@Deprecated(since = "3.34", forRemoval = true)
@SuppressWarnings("removal")
private Set<OpenApiFilter.RunStage> resolveStages(AnnotationInstance ai, IndexView index) {

// remember: AnnotationInstance.value does NOT return default values, and instead return null if not explicitly set

Set<OpenApiFilter.RunStage> runStages = EnumSet.noneOf(OpenApiFilter.RunStage.class);
AnnotationValue stages = ai.value("stages");
if (stages != null) {
for (AnnotationValue sv : stages.asArrayList()) {
runStages.add(OpenApiFilter.RunStage.valueOf(sv.asEnum()));
}
} else {
AnnotationValue value = ai.value();
if (value != null) {
runStages.add(OpenApiFilter.RunStage.valueOf(value.asEnum()));
} else {
stages = ai.valueWithDefault(index, "stages");
for (AnnotationValue sv : stages.asArrayList()) {
runStages.add(OpenApiFilter.RunStage.valueOf(sv.asEnum()));
}
}
}

if (runStages.remove(OpenApiFilter.RunStage.BOTH)) {
runStages.add(OpenApiFilter.RunStage.BUILD);
runStages.add(OpenApiFilter.RunStage.RUN);
}

return runStages;
}

private List<String> getUserDefinedFilters(IndexView index, String documentName,
OpenApiFilter.RunStage... requestedStages) {
Comparator<Object> comparator = Comparator
.comparing(x -> ((AnnotationInstance) x).valueWithDefault(index, "priority").asInt())
.reversed();

return index
.getAnnotations(OpenApiFilter.class)
.stream()
.filter(ai -> stages.contains(OpenApiFilter.RunStage.valueOf(ai.valueWithDefault(index).asEnum())))
.filter(ai -> {
Set<OpenApiFilter.RunStage> resolved = resolveStages(ai, index);
for (OpenApiFilter.RunStage stage : requestedStages) {
if (resolved.contains(stage)) {
return true;
}
}
return false;
})
.filter(ai -> {
List<String> documentNames = extractDocumentNames(index, ai);
for (String dn : documentNames) {
Expand Down Expand Up @@ -1161,7 +1261,7 @@ private SmallRyeOpenAPI buildOpenApiDocument(
.enableStandardFilter(false)
.withFilters(oasFilters);

getUserDefinedBuildTimeFilters(index, documentName).forEach(builder::addFilterName);
getUserDefinedFilters(index, documentName, OpenApiFilter.RunStage.BUILD).forEach(builder::addFilterName);

// This should be the final filter to run
builder.addFilter(new DefaultInfoFilter(config));
Expand Down Expand Up @@ -1192,7 +1292,7 @@ private SmallRyeOpenAPI applyRuntimeFilters(

try {
SmallRyeOpenAPI.Builder builder = filterOnlyBuilder.get();
getUserDefinedRuntimeFilters(config, index, documentName).forEach(builder::addFilterName);
getUserDefinedRuntimeStartupFilters(config, index, documentName).forEach(builder::addFilterName);
return builder.build();
} catch (Exception e) {
// Try again without the user-defined runtime filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import io.quarkus.smallrye.openapi.OpenApiFilter;

@OpenApiFilter(OpenApiFilter.RunStage.BUILD)
@OpenApiFilter(stages = OpenApiFilter.RunStage.BUILD)
public class CounterBuildtimeFilter implements OASFilter {

private static final AtomicInteger TIMES = new AtomicInteger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class ManagedBeanOASFilterTest {
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alice=alice
quarkus.security.users.embedded.users.bob=bob
quarkus.smallrye-openapi.always-run-filter=true
"""),
"application.properties"))
.setForcedDependencies(List.of(
Expand All @@ -49,7 +48,7 @@ class ManagedBeanOASFilterTest {
Dependency.of("io.quarkus", "quarkus-elytron-security-properties-file", Version.getVersion())));

@RequestScoped
@OpenApiFilter(value = RunStage.RUN, priority = 99)
@OpenApiFilter(stages = RunStage.RUNTIME_PER_REQUEST, priority = 99)
public static class MyFilter1 implements OASFilter {
@Inject
HttpServerRequest req;
Expand All @@ -66,7 +65,7 @@ public void filterOpenAPI(OpenAPI openAPI) {
}

@ApplicationScoped
@OpenApiFilter(value = RunStage.RUN, priority = 98)
@OpenApiFilter(stages = RunStage.RUNTIME_PER_REQUEST, priority = 98)
public static class MyFilter2 implements OASFilter {
@Inject
HttpServerRequest req;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/**
* Filter to add custom elements
*/
@OpenApiFilter(OpenApiFilter.RunStage.BUILD)
@OpenApiFilter(stages = OpenApiFilter.RunStage.BUILD)
public class MyBuildTimeFilter implements OASFilter {

private IndexView view;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Filter to add custom elements
*/
@OpenApiFilter(value = OpenApiFilter.RunStage.BUILD, priority = 0)
@OpenApiFilter(stages = OpenApiFilter.RunStage.BUILD, priority = 0)
public class MyBuildTimeFilterPrio0 implements OASFilter {

private IndexView view;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Filter to add custom elements
*/
@OpenApiFilter(value = OpenApiFilter.RunStage.BUILD, priority = 2)
@OpenApiFilter(stages = OpenApiFilter.RunStage.BUILD, priority = 2)
public class MyBuildTimeFilterPrio2 implements OASFilter {

private IndexView view;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Filter to add custom elements
*/
@OpenApiFilter(OpenApiFilter.RunStage.BUILD)
@OpenApiFilter(stages = OpenApiFilter.RunStage.BUILD)
public class MyBuildTimeFilterPrioDefault implements OASFilter {

private IndexView view;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/**
* Filter to add custom elements
*/
@OpenApiFilter(OpenApiFilter.RunStage.RUN)
@OpenApiFilter
public class MyRunTimeFilter implements OASFilter {

@Override
Expand Down
Loading
Loading