diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 705641f70..ad0ca244e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,14 +86,14 @@ jobs: java-version: 11 - name: maven cache - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: generate javadocs - run: mvn -B package javadoc:javadoc -DskipTests + run: mvn -B install javadoc:javadoc -DskipTests tck-reporting: runs-on: ubuntu-latest 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 2918ae6b9..07e4e6cad 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/OpenApiProcessor.java @@ -2,9 +2,11 @@ import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Constructor; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.Callable; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -12,6 +14,7 @@ import org.eclipse.microprofile.openapi.OASModelReader; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.OpenApiDocument; @@ -27,6 +30,8 @@ */ public class OpenApiProcessor { + static final IndexView EMPTY_INDEX = new Indexer().complete(); + private OpenApiProcessor() { } @@ -71,8 +76,8 @@ public static OpenAPI bootstrap(OpenApiConfig config, IndexView index, ClassLoad } // Filter and model if (config != null && classLoader != null) { - OpenApiDocument.INSTANCE.modelFromReader(modelFromReader(config, classLoader)); - OpenApiDocument.INSTANCE.filter(getFilter(config, classLoader)); + OpenApiDocument.INSTANCE.modelFromReader(modelFromReader(config, classLoader, index)); + OpenApiDocument.INSTANCE.filter(getFilter(config, classLoader, index)); } OpenApiDocument.INSTANCE.initialize(); @@ -169,20 +174,27 @@ public static OpenAPI modelFromAnnotations(OpenApiConfig config, ClassLoader loa * @param config OpenApiConfig * @param loader ClassLoader * @return OpenApiImpl created from OASModelReader + * + * @deprecated use {@linkplain #modelFromReader(OpenApiConfig, ClassLoader, IndexView)} instead */ + @Deprecated public static OpenAPI modelFromReader(OpenApiConfig config, ClassLoader loader) { - String readerClassName = config.modelReader(); - if (readerClassName == null) { - return null; - } - try { - Class c = loader.loadClass(readerClassName); - OASModelReader reader = (OASModelReader) c.getDeclaredConstructor().newInstance(); - return reader.buildModel(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException | NoSuchMethodException | SecurityException e) { - throw new OpenApiRuntimeException(e); - } + return modelFromReader(config, loader, EMPTY_INDEX); + } + + /** + * Instantiate the configured {@link OASModelReader} and invoke it. If no reader is configured, + * then return null. If a class is configured but there is an error either instantiating or invoking + * it, a {@link OpenApiRuntimeException} is thrown. + * + * @param config OpenApiConfig + * @param loader ClassLoader + * @param index an IndexView to be provided to the filter when accepted via its constructor + * @return OpenApiImpl created from OASModelReader + */ + public static OpenAPI modelFromReader(OpenApiConfig config, ClassLoader loader, IndexView index) { + OASModelReader reader = newInstance(config.modelReader(), loader, index); + return reader != null ? reader.buildModel() : null; } /** @@ -191,17 +203,49 @@ public static OpenAPI modelFromReader(OpenApiConfig config, ClassLoader loader) * @param config OpenApiConfig * @param loader ClassLoader * @return OASFilter instance retrieved from loader + * + * @deprecated use {@linkplain #getFilter(OpenApiConfig, ClassLoader, IndexView)} instead */ + @Deprecated public static OASFilter getFilter(OpenApiConfig config, ClassLoader loader) { - String filterClassName = config.filter(); - if (filterClassName == null) { + return getFilter(config, loader, EMPTY_INDEX); + } + + /** + * Instantiate the {@link OASFilter} configured by the application. + * + * @param config OpenApiConfig + * @param loader ClassLoader + * @param index an IndexView to be provided to the filter when accepted via its constructor + * @return OASFilter instance retrieved from loader + */ + public static OASFilter getFilter(OpenApiConfig config, ClassLoader loader, IndexView index) { + return newInstance(config.filter(), loader, index); + } + + @SuppressWarnings("unchecked") + static T newInstance(String className, ClassLoader loader, IndexView index) { + if (className == null) { return null; } + + Class klazz = uncheckedCall(() -> (Class) loader.loadClass(className)); + + return Arrays.stream(klazz.getDeclaredConstructors()) + .filter(OpenApiProcessor::acceptsIndexView) + .findFirst() + .map(ctor -> uncheckedCall(() -> (T) ctor.newInstance(index))) + .orElseGet(() -> uncheckedCall(() -> klazz.getDeclaredConstructor().newInstance())); + } + + private static boolean acceptsIndexView(Constructor ctor) { + return ctor.getParameterCount() == 1 && IndexView.class.isAssignableFrom(ctor.getParameterTypes()[0]); + } + + private static T uncheckedCall(Callable callable) { try { - Class c = loader.loadClass(filterClassName); - return (OASFilter) c.getDeclaredConstructor().newInstance(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException | NoSuchMethodException | SecurityException e) { + return callable.call(); + } catch (Exception e) { throw new OpenApiRuntimeException(e); } } diff --git a/core/src/test/java/io/smallrye/openapi/runtime/OpenApiProcessorTest.java b/core/src/test/java/io/smallrye/openapi/runtime/OpenApiProcessorTest.java new file mode 100644 index 000000000..7201817d7 --- /dev/null +++ b/core/src/test/java/io/smallrye/openapi/runtime/OpenApiProcessorTest.java @@ -0,0 +1,86 @@ +package io.smallrye.openapi.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.jboss.jandex.IndexView; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OpenApiProcessorTest { + + ClassLoader loader; + + @BeforeEach + void setup() { + this.loader = Thread.currentThread().getContextClassLoader(); + } + + @Test + void testNewInstanceWithNullClassName() { + Object instance = OpenApiProcessor.newInstance(null, loader, OpenApiProcessor.EMPTY_INDEX); + assertNull(instance); + } + + @Test + void testNewInstanceWithNotFoundClassName() { + String invalidClassName = UUID.randomUUID().toString(); + + Throwable thrown = assertThrows(OpenApiRuntimeException.class, + () -> OpenApiProcessor.newInstance(invalidClassName, loader, OpenApiProcessor.EMPTY_INDEX)); + assertEquals(ClassNotFoundException.class, thrown.getCause().getClass()); + } + + @Test + void testNewInstanceWithEmptyIndex() { + IndexAwareObject instance = OpenApiProcessor.newInstance(IndexAwareObject.class.getName(), loader, + OpenApiProcessor.EMPTY_INDEX); + assertNotNull(instance); + assertFalse(instance.defaultConstructorUsed); + assertEquals(0, instance.index.getKnownClasses().size()); + } + + @Test + void testNewInstanceWithIndexUnsupported() { + IndexUnawareObject instance = OpenApiProcessor.newInstance(IndexUnawareObject.class.getName(), loader, + OpenApiProcessor.EMPTY_INDEX); + assertNotNull(instance); + assertTrue(instance.defaultConstructorUsed); + } + + static class IndexAwareObject { + IndexView index; + boolean defaultConstructorUsed; + + IndexAwareObject() { + defaultConstructorUsed = true; + } + + IndexAwareObject(IndexView index) { + this.index = index; + defaultConstructorUsed = false; + } + + IndexAwareObject(Object other) { + defaultConstructorUsed = false; + } + } + + static class IndexUnawareObject { + boolean defaultConstructorUsed; + + IndexUnawareObject() { + defaultConstructorUsed = true; + } + + IndexUnawareObject(Object other) { + defaultConstructorUsed = false; + } + } +} diff --git a/testsuite/tck/src/test/java/io/smallrye/openapi/tck/DeploymentProcessor.java b/testsuite/tck/src/test/java/io/smallrye/openapi/tck/DeploymentProcessor.java index ffbcb742b..afe486a90 100644 --- a/testsuite/tck/src/test/java/io/smallrye/openapi/tck/DeploymentProcessor.java +++ b/testsuite/tck/src/test/java/io/smallrye/openapi/tck/DeploymentProcessor.java @@ -13,6 +13,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.util.Collection; import java.util.Optional; import java.util.Properties; @@ -20,6 +21,7 @@ import java.util.logging.Logger; import java.util.stream.Stream; +import org.apache.commons.io.output.ByteArrayOutputStream; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription; @@ -27,6 +29,7 @@ import org.jboss.arquillian.core.api.annotation.Observes; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; +import org.jboss.jandex.IndexWriter; import org.jboss.jandex.Indexer; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.ArchivePaths; @@ -114,7 +117,7 @@ private static void generateOpenAPI(final WebArchive war) { ClassLoader contextClassLoader = currentThread().getContextClassLoader(); Optional annotationModel = ofNullable(modelFromAnnotations(openApiConfig, contextClassLoader, index)); - Optional readerModel = ofNullable(modelFromReader(openApiConfig, contextClassLoader)); + Optional readerModel = ofNullable(modelFromReader(openApiConfig, contextClassLoader, index)); Optional staticFileModel = Stream.of(modelFromFile(openApiConfig, war, "/META-INF/openapi.json", JSON), modelFromFile(openApiConfig, war, "/META-INF/openapi.yaml", YAML), modelFromFile(openApiConfig, war, "/META-INF/openapi.yml", YAML)) @@ -128,7 +131,7 @@ private static void generateOpenAPI(final WebArchive war) { annotationModel.ifPresent(document::modelFromAnnotations); readerModel.ifPresent(document::modelFromReader); staticFileModel.ifPresent(document::modelFromStaticFile); - document.filter(getFilter(openApiConfig, contextClassLoader)); + document.filter(getFilter(openApiConfig, contextClassLoader, index)); document.initialize(); OpenAPI openAPI = document.get(); @@ -139,6 +142,13 @@ private static void generateOpenAPI(final WebArchive war) { // Ignore } + try (ByteArrayOutputStream indexOut = new ByteArrayOutputStream()) { + new IndexWriter(indexOut).write(index); + war.addAsManifestResource(new ByteArrayAsset(indexOut.toByteArray()), "jandex.idx"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + document.reset(); } diff --git a/testsuite/tck/src/test/java/io/smallrye/openapi/tck/OpenApiRegistration.java b/testsuite/tck/src/test/java/io/smallrye/openapi/tck/OpenApiRegistration.java index 9f775dd63..694b79f07 100644 --- a/testsuite/tck/src/test/java/io/smallrye/openapi/tck/OpenApiRegistration.java +++ b/testsuite/tck/src/test/java/io/smallrye/openapi/tck/OpenApiRegistration.java @@ -3,6 +3,7 @@ import static io.smallrye.openapi.runtime.io.Format.JSON; import static io.smallrye.openapi.runtime.io.Format.YAML; +import java.io.InputStream; import java.net.URL; import java.util.Map; import java.util.Optional; @@ -17,6 +18,8 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.IndexReader; +import org.jboss.jandex.IndexView; import org.jboss.resteasy.spi.ResteasyDeployment; import io.smallrye.openapi.api.OpenApiConfig; @@ -72,15 +75,21 @@ private Optional readOpenApiFile(final ServletContext servletContext, f return Optional.empty(); } + final IndexView index; + + try (InputStream indexStream = servletContext.getResourceAsStream("/META-INF/jandex.idx")) { + index = new IndexReader(indexStream).read(); + } + final OpenApiDocument document = OpenApiDocument.INSTANCE; + try (OpenApiStaticFile staticFile = new OpenApiStaticFile(resource.openStream(), format)) { Config config = ConfigProvider.getConfig(); OpenApiConfig openApiConfig = OpenApiConfig.fromConfig(config); document.reset(); document.config(openApiConfig); - document.filter(OpenApiProcessor.getFilter(openApiConfig, Thread.currentThread().getContextClassLoader())); - document.modelFromStaticFile( - io.smallrye.openapi.runtime.OpenApiProcessor.modelFromStaticFile(openApiConfig, staticFile)); + document.filter(OpenApiProcessor.getFilter(openApiConfig, Thread.currentThread().getContextClassLoader(), index)); + document.modelFromStaticFile(OpenApiProcessor.modelFromStaticFile(openApiConfig, staticFile)); document.initialize(); return Optional.of(document.get()); } finally { diff --git a/tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiTask.java b/tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiTask.java index b4ee0fde1..293b96aec 100644 --- a/tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiTask.java +++ b/tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiTask.java @@ -127,7 +127,7 @@ private OpenApiDocument generateSchema( OpenAPI staticModel = generateStaticModel(openApiConfig, resourcesSrcDirs); OpenAPI annotationModel = generateAnnotationModel(index, openApiConfig, SmallryeOpenApiTask.class.getClassLoader()); - OpenAPI readerModel = OpenApiProcessor.modelFromReader(openApiConfig, classLoader); + OpenAPI readerModel = OpenApiProcessor.modelFromReader(openApiConfig, classLoader, index); OpenApiDocument document = OpenApiDocument.INSTANCE; @@ -146,7 +146,7 @@ private OpenApiDocument generateSchema( addingModelDebug("static", staticModel); document.modelFromStaticFile(staticModel); } - document.filter(OpenApiProcessor.getFilter(openApiConfig, classLoader)); + document.filter(OpenApiProcessor.getFilter(openApiConfig, classLoader, index)); document.initialize(); return document; diff --git a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java index 954f2f0e2..21c1368a9 100644 --- a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java +++ b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java @@ -288,7 +288,7 @@ private OpenApiDocument generateSchema(IndexView index) throws IOException, Depe OpenAPI staticModel = generateStaticModel(openApiConfig); OpenAPI annotationModel = generateAnnotationModel(index, openApiConfig, classLoader); - OpenAPI readerModel = OpenApiProcessor.modelFromReader(openApiConfig, classLoader); + OpenAPI readerModel = OpenApiProcessor.modelFromReader(openApiConfig, classLoader, index); OpenApiDocument document = OpenApiDocument.newInstance(); @@ -304,7 +304,7 @@ private OpenApiDocument generateSchema(IndexView index) throws IOException, Depe if (staticModel != null) { document.modelFromStaticFile(staticModel); } - document.filter(OpenApiProcessor.getFilter(openApiConfig, classLoader)); + document.filter(OpenApiProcessor.getFilter(openApiConfig, classLoader, index)); document.initialize(); return document;