Skip to content

Commit

Permalink
Merge pull request #36152 from phillip-kruger/openapi-build-filter
Browse files Browse the repository at this point in the history
Allow Build time OpenAPI Filters
  • Loading branch information
phillip-kruger authored Sep 27, 2023
2 parents c8a911c + 0c06947 commit 05ea722
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 15 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<smallrye-config.version>3.3.4</smallrye-config.version>
<smallrye-health.version>4.0.4</smallrye-health.version>
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
<smallrye-open-api.version>3.5.2</smallrye-open-api.version>
<smallrye-open-api.version>3.6.0</smallrye-open-api.version>
<smallrye-graphql.version>2.4.0</smallrye-graphql.version>
<smallrye-opentracing.version>3.0.3</smallrye-opentracing.version>
<smallrye-fault-tolerance.version>6.2.6</smallrye-fault-tolerance.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -234,6 +235,23 @@ void registerAutoSecurityFilter(BuildProducer<SyntheticBeanBuildItem> syntheticB
.supplier(recorder.autoSecurityFilterSupplier(autoSecurityFilter)).done());
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
OpenApiRecorder recorder) {
Config config = ConfigProvider.getConfig();
OpenApiConfig openApiConfig = new OpenApiConfigImpl(config);

List<String> 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,
Expand Down Expand Up @@ -432,6 +450,37 @@ void addAutoFilters(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenAPID
}
}

private List<String> getUserDefinedBuildtimeFilters(OpenApiConfig openApiConfig, IndexView index) {
return getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.BUILD);
}

private List<String> getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, IndexView index) {
List<String> 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<String> getUserDefinedFilters(OpenApiConfig openApiConfig, IndexView index, OpenApiFilter.RunStage stage) {
List<String> userDefinedFilters = new ArrayList<>();
Collection<AnnotationInstance> 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) {
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -1151,6 +1203,11 @@ private OpenApiDocument prepareOpenApiDocument(OpenAPI staticModel,
OASFilter otherExtensionFilter = openAPIBuildItem.getOASFilter();
document.filter(otherExtensionFilter);
}
// Add user defined Build time filters
List<String> userDefinedFilters = getUserDefinedBuildtimeFilters(openApiConfig, index);
for (String filter : userDefinedFilters) {
document.filter(filter(filter, index));
}
return document;
}

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ClassInfo> knownClasses = this.view.getKnownClasses();
Info info = OASFactory.createInfo();
info.setDescription("Created from Annotated Buildtime filter with " + knownClasses.size() + " known indexed classes");
openAPI.setInfo(info);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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"));

}

}
Original file line number Diff line number Diff line change
@@ -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"));

}

}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,18 +36,18 @@ 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()) {
System.setProperty(OPENAPI_SERVERS, servers);
}

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());
}
}

Expand Down Expand Up @@ -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<String> userFilters) {
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
: OpenApiConstants.classLoader;
try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) {
Expand All @@ -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)
Expand Down Expand Up @@ -125,18 +129,26 @@ static class DynamicDocument implements OpenApiDocumentHolder {

private OpenAPI generatedOnBuild;
private OpenApiConfig openApiConfig;
private OASFilter userFilter;
private List<OASFilter> userFilters = new ArrayList<>();
private OASFilter autoFilter;
private DisabledRestEndpointsFilter disabledEndpointsFilter;

DynamicDocument(Config config, OASFilter autoFilter) {
DynamicDocument(Config config, OASFilter autoFilter, List<String> 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();
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -109,4 +110,22 @@ public OASFilter get() {
}
};
}

public Supplier<?> createUserDefinedRuntimeFilters(List<String> filters) {
return new Supplier<Object>() {
@Override
public UserDefinedRuntimeFilters get() {
return new UserDefinedRuntimeFilters() {
@Override
public List<String> filters() {
return filters;
}
};
}
};
}

public interface UserDefinedRuntimeFilters {
List<String> filters();
}
}

0 comments on commit 05ea722

Please sign in to comment.