From 1ac5220508e78b8bfe75c80f9852c0d8925591f3 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 13 Sep 2023 07:37:58 +0800 Subject: [PATCH] Allow client-supplied `AnnotationScanner`s, bypassing service loader (#1567) Signed-off-by: Michael Edgar --- .../openapi/runtime/OpenApiProcessor.java | 46 +++++++++++-- .../scanner/AnnotationScannerExtension.java | 8 +++ .../scanner/OpenApiAnnotationScanner.java | 64 ++++++++++++++----- .../spi/AbstractParameterProcessor.java | 2 - .../scanner/spi/AnnotationScannerFactory.java | 40 ++++++++---- .../scanner/JaxRsAnnotationScannerTest.java | 28 ++++---- 6 files changed, 135 insertions(+), 53 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java index 07e4e6cad..c817133e4 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import java.util.function.Supplier; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -21,7 +22,10 @@ import io.smallrye.openapi.api.util.ClassLoaderUtil; import io.smallrye.openapi.runtime.io.Format; import io.smallrye.openapi.runtime.io.OpenApiParser; +import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner; +import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner; +import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerFactory; /** * Provides some core archive processing functionality. @@ -148,21 +152,49 @@ public static OpenAPI modelFromAnnotations(OpenApiConfig config, IndexView index } /** - * Create an {@link OpenAPI} model by scanning the deployment for relevant JAX-RS and - * OpenAPI annotations. If scanning is disabled, this method returns null. If scanning - * is enabled but no relevant annotations are found, an empty OpenAPI model is returned. + * Create an {@link OpenAPI} model by scanning the deployment for relevant + * JAX-RS and OpenAPI annotations. If scanning is disabled, this method + * returns null. If scanning is enabled but no relevant annotations are + * found, an empty OpenAPI model is returned. * - * @param config OpenApiConfig - * @param loader ClassLoader - * @param index IndexView of Archive + * @param config + * OpenApiConfig + * @param loader + * ClassLoader to discover AnnotationScanner services (via + * ServiceLoader) as well as loading application classes + * @param index + * IndexView of Archive * @return OpenAPIImpl generated from annotations */ public static OpenAPI modelFromAnnotations(OpenApiConfig config, ClassLoader loader, IndexView index) { + return modelFromAnnotations(config, loader, index, new AnnotationScannerFactory(loader)); + } + + /** + * Create an {@link OpenAPI} model by scanning the deployment for relevant + * JAX-RS and OpenAPI annotations. If scanning is disabled, this method + * returns null. If scanning is enabled but no relevant annotations are + * found, an empty OpenAPI model is returned. + * + * @param config + * OpenApiConfig + * @param loader + * ClassLoader to load application classes + * @param index + * IndexView of Archive + * @param scannerSupplier + * supplier of AnnotationScanner instances to use to generate the + * OpenAPI model for the application + * @return OpenAPI generated from annotations + */ + public static OpenAPI modelFromAnnotations(OpenApiConfig config, ClassLoader loader, IndexView index, + Supplier> scannerSupplier) { if (config.scanDisable()) { return null; } - OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, loader, index); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, loader, index, scannerSupplier, + AnnotationScannerExtension.DEFAULT); return scanner.scan(); } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/AnnotationScannerExtension.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/AnnotationScannerExtension.java index 5f47c109d..f76cf797e 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/AnnotationScannerExtension.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/AnnotationScannerExtension.java @@ -1,6 +1,8 @@ package io.smallrye.openapi.runtime.scanner; import java.util.Collection; +import java.util.Collections; +import java.util.List; import org.eclipse.microprofile.openapi.models.media.Schema; import org.jboss.jandex.AnnotationInstance; @@ -17,6 +19,12 @@ */ public interface AnnotationScannerExtension { + static final List DEFAULT = Collections.singletonList(new Default()); + + static class Default implements AnnotationScannerExtension { + // All default methods + } + /** * Unwraps an asynchronous type such as * CompletionStage<X> into its resolved type diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiAnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiAnnotationScanner.java index 62cc239cc..626995257 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiAnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiAnnotationScanner.java @@ -11,7 +11,9 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.eclipse.microprofile.openapi.models.Components; import org.eclipse.microprofile.openapi.models.OpenAPI; @@ -48,7 +50,7 @@ public class OpenApiAnnotationScanner { private final AnnotationScannerContext annotationScannerContext; - private final AnnotationScannerFactory annotationScannerFactory; + private final Supplier> scannerSupplier; /** * Constructor. @@ -57,9 +59,7 @@ public class OpenApiAnnotationScanner { * @param index IndexView of deployment */ public OpenApiAnnotationScanner(OpenApiConfig config, IndexView index) { - this(config, ClassLoaderUtil.getDefaultClassLoader(), index, - Collections.singletonList(new AnnotationScannerExtension() { - })); + this(config, ClassLoaderUtil.getDefaultClassLoader(), index, AnnotationScannerExtension.DEFAULT); } /** @@ -76,22 +76,53 @@ public OpenApiAnnotationScanner(OpenApiConfig config, IndexView index, List extensions) { + this(config, loader, index, new AnnotationScannerFactory(loader), extensions); + } + + /** + * Constructor. + * + * @param config + * OpenApiConfig instance + * @param loader + * ClassLoader to load application classes + * @param index + * IndexView of deployment + * @param scannerSupplier + * supplier of AnnotationScanner instances to use to generate the + * OpenAPI model for the application + * @param extensions + * A set of extensions to scanning */ public OpenApiAnnotationScanner(OpenApiConfig config, ClassLoader loader, IndexView index, + Supplier> scannerSupplier, List extensions) { FilteredIndexView filteredIndexView; @@ -103,7 +134,7 @@ public OpenApiAnnotationScanner(OpenApiConfig config, ClassLoader loader, IndexV this.annotationScannerContext = new AnnotationScannerContext(filteredIndexView, loader, extensions, config, new OpenAPIImpl()); - this.annotationScannerFactory = new AnnotationScannerFactory(loader); + this.scannerSupplier = scannerSupplier; } /** @@ -130,15 +161,16 @@ public OpenAPI scan(String... filter) { return openApi; } - private List getScanners(String[] filters) { - List knownScanners = annotationScannerFactory.getAnnotationScanners(); + private Iterable getScanners(String[] filters) { List enabledScanners = Optional.ofNullable(filters).map(Arrays::asList).orElseGet(Collections::emptyList); if (enabledScanners.isEmpty()) { - return knownScanners; + return scannerSupplier.get(); } - return knownScanners.stream().filter(s -> enabledScanners.contains(s.getName())).collect(Collectors.toList()); + return StreamSupport.stream(scannerSupplier.get().spliterator(), false) + .filter(s -> enabledScanners.contains(s.getName())) + .collect(Collectors.toList()); } private OpenAPI scanMicroProfileOpenApiAnnotations() { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index ebf47f368..d72d88313 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -81,7 +81,6 @@ public abstract class AbstractParameterProcessor { protected final AnnotationScannerContext scannerContext; protected final String contextPath; protected final IndexView index; - protected final ClassLoader cl; protected final Function readerFunction; protected final List extensions; protected final Optional beanValidationScanner; @@ -228,7 +227,6 @@ protected AbstractParameterProcessor(AnnotationScannerContext scannerContext, this.scannerContext = scannerContext; this.contextPath = contextPath; this.index = scannerContext.getIndex(); - this.cl = scannerContext.getClassLoader(); this.readerFunction = reader; this.extensions = extensions; this.beanValidationScanner = scannerContext.getBeanValidationScanner(); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerFactory.java index 2370dee26..376153eac 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerFactory.java @@ -1,30 +1,42 @@ package io.smallrye.openapi.runtime.scanner.spi; +import static java.util.Comparator.comparing; +import static java.util.Comparator.nullsLast; + import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.ServiceLoader; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * Factory that allows plugging in more scanners. * * @author Phillip Kruger (phillip.kruger@redhat.com) */ -public class AnnotationScannerFactory { - private final Map loadedScanners = new HashMap<>(); - - public AnnotationScannerFactory(ClassLoader cl) { - ServiceLoader loader = ServiceLoader.load(AnnotationScanner.class, cl); - Iterator scannerIterator = loader.iterator(); - while (scannerIterator.hasNext()) { - AnnotationScanner scanner = scannerIterator.next(); - loadedScanners.put(scanner.getName(), scanner); - } +public class AnnotationScannerFactory implements Supplier> { + + /** + * List of AnnotationScanners discovered via the ServiceLoader, ordered by + * {@linkplain AnnotationScanner#getName() name} + */ + private final List loadedScanners; + + public AnnotationScannerFactory(ClassLoader loader) { + Iterable scanners = ServiceLoader.load(AnnotationScanner.class, loader); + loadedScanners = StreamSupport.stream(scanners.spliterator(), false) + .sorted(comparing(AnnotationScanner::getName, nullsLast(String::compareTo))) + .collect(Collectors.toList()); } public List getAnnotationScanners() { - return new ArrayList<>(loadedScanners.values()); + return new ArrayList<>(loadedScanners); } + + @Override + public Iterable get() { + return getAnnotationScanners(); + } + } diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java index f94cff87f..bc71f1582 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java @@ -67,7 +67,7 @@ void testJakartaHiddenOperationNotPresent() throws IOException, JSONException { void testHiddenOperationNotPresent(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.testHiddenOperationNotPresent.json", result); @@ -96,7 +96,7 @@ void testJakartaHiddenOperationPathNotPresent() throws IOException, JSONExceptio void testHiddenOperationPathNotPresent(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.testHiddenOperationPathNotPresent.json", result); @@ -138,7 +138,7 @@ void testRequestBodyComponentGeneration(Index i) throws IOException, JSONExcepti MyCustomSchemaRegistry.class.getName()); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.testRequestBodyComponentGeneration.json", result); @@ -169,7 +169,7 @@ void testJakartaPackageInfoDefinitionScanning() throws IOException, JSONExceptio void testPackageInfoDefinitionScanning(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.testPackageInfoDefinitionScanning.json", result); @@ -197,7 +197,7 @@ void testJakartaTagScanning() throws IOException, JSONException { void testTagScanning(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig(new HashMap()), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.tags.multilocation.json", result); } @@ -224,7 +224,7 @@ void testJakartaTagScanning_OrderGivenAnnotations() throws IOException, JSONExce void testTagScanning_OrderGivenAnnotations(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig(new HashMap()), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.tags.ordergiven.annotation.json", result); } @@ -249,7 +249,7 @@ void testJakartaTagScanning_OrderGivenStaticFile() throws IOException, JSONExcep void testTagScanning_OrderGivenStaticFile(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig(new HashMap()), i); - OpenAPI scanResult = scanner.scan(); + OpenAPI scanResult = scanner.scan("JAX-RS"); OpenAPI staticResult = OpenApiParser.parse(new ByteArrayInputStream( "{\"info\" : {\"title\" : \"Tag order in static file\",\"version\" : \"1.0.0-static\"},\"tags\": [{\"name\":\"tag3\"},{\"name\":\"tag1\"}]}" .getBytes()), @@ -284,7 +284,7 @@ void testJakartaEmptySecurityRequirements() throws IOException, JSONException { void testEmptySecurityRequirements(Index i) throws IOException, JSONException { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), i); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.testEmptySecurityRequirements.json", result); @@ -297,7 +297,7 @@ void testInterfaceWithoutImplentationExcluded() throws IOException, JSONExceptio Index index = indexOf(MissingImplementation.class); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("default.json", result); @@ -315,7 +315,7 @@ void testInterfaceWithoutImplentationIncluded(String configKey, String configVal Index index = indexOf(MissingImplementation.class); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig(configKey, configValue), index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.interface-only.json", result); @@ -335,7 +335,7 @@ void testInterfaceWithConcreteImplentation() throws IOException, JSONException { Index index = indexOf(HasConcreteImplementation.class, ImplementsHasConcreteImplementation.class); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("resource.concrete-implementation.json", result); @@ -362,7 +362,7 @@ void testInterfaceWithAbstractImplentation() throws IOException, JSONException { Index index = indexOf(HasAbstractImplementation.class, ImplementsHasAbstractImplementation.class); OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); printToConsole(result); assertJsonEquals("default.json", result); @@ -412,7 +412,7 @@ void testIncludeProfile() { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); Assertions.assertEquals(1, result.getPaths().getPathItems().size()); Assertions.assertTrue(result.getPaths().getPathItems().containsKey("/profile/{id}")); @@ -426,7 +426,7 @@ void testExcludeProfile() { OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); - OpenAPI result = scanner.scan(); + OpenAPI result = scanner.scan("JAX-RS"); Assertions.assertEquals(1, result.getPaths().getPathItems().size()); Assertions.assertTrue(result.getPaths().getPathItems().containsKey("/profile"));