This is an automated email from the ASF dual-hosted git repository. jlmonteiro pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomee.git
commit a79397ac32b10ba969095b40b73c6be1e783ae37 Author: Jean-Louis Monteiro <[email protected]> AuthorDate: Fri Sep 2 11:37:53 2022 +0200 TOMEE-3900 Integrate SmallRye OpenAPI and pass TCK --- .../java/org/apache/openejb/util/AppFinder.java | 4 + tck/microprofile-tck/openapi/pom.xml | 41 +++-- .../MicroProfileOpenAPITCKDeploymentPackager.java | 7 +- .../MicroProfileOpenAPITCKExtension.java | 2 + .../MicroProfileOpenApiDeploymentProcessor.java | 185 +++++++++++++++++++++ .../openapi/src/test/resources/arquillian.xml | 1 + tck/microprofile-tck/openapi/tck-dev.xml | 25 +++ tck/microprofile-tck/opentracing/pom.xml | 2 +- tck/microprofile-tck/pom.xml | 2 +- .../microprofile/TomEEMicroProfileListener.java | 3 +- ...r.java => MicroProfileMetricsRegistration.java} | 9 +- .../openapi/MicroProfileOpenApiEndpoint.java | 59 +++++++ .../openapi/MicroProfileOpenApiRegistration.java | 116 +++++++++++++ .../jakarta.servlet.ServletContainerInitializer | 3 +- 14 files changed, 431 insertions(+), 28 deletions(-) diff --git a/container/openejb-core/src/main/java/org/apache/openejb/util/AppFinder.java b/container/openejb-core/src/main/java/org/apache/openejb/util/AppFinder.java index cd8cf81103..dd53848e42 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/util/AppFinder.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/util/AppFinder.java @@ -25,6 +25,10 @@ import org.apache.webbeans.config.WebBeansContext; public final class AppFinder { public static <T> T findAppContextOrWeb(final ClassLoader cl, final Transformer<T> transformer) { final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class); + if (containerSystem == null) { + return null; // when used before the container even gets a chance to start + } + for (final AppContext appContext : containerSystem.getAppContexts()) { final ClassLoader appContextClassLoader = appContext.getClassLoader(); boolean found = false; diff --git a/tck/microprofile-tck/openapi/pom.xml b/tck/microprofile-tck/openapi/pom.xml index bd240b0ce1..c0fc18ce45 100644 --- a/tck/microprofile-tck/openapi/pom.xml +++ b/tck/microprofile-tck/openapi/pom.xml @@ -38,15 +38,6 @@ <dependenciesToScan> <dependency>org.eclipse.microprofile.openapi:microprofile-openapi-tck</dependency> </dependenciesToScan> - <excludes> - <!-- TODO - Expecting UserResource to be deployed, but not included in the JAXRSApp. Open issue in project. --> - <exclude>org.eclipse.microprofile.openapi.tck.OASConfigServersTest</exclude> - <!-- TODO - Possible bug in the implementation - Test fail due to Health endpoint being added to the OpenAPI result --> - <!-- Only endpoints set up in Application should be added. Check org.apache.geronimo.microprofile.openapi.cdi.GeronimoOpenAPIExtension.createOpenApi --> - <exclude>org.eclipse.microprofile.openapi.tck.OASConfigExcludePackageTest</exclude> - <exclude>org.eclipse.microprofile.openapi.tck.OASConfigExcludeClassTest</exclude> - <exclude>org.eclipse.microprofile.openapi.tck.OASConfigExcludeClassesTest</exclude> - </excludes> <systemPropertyVariables> <test.url>http://localhost:50290</test.url> </systemPropertyVariables> @@ -108,8 +99,8 @@ <scope>test</scope> <exclusions> <exclusion> - <groupId>javax.ws.rs</groupId> - <artifactId>javax.ws.rs-api</artifactId> + <groupId>jakarta.ws.rs</groupId> + <artifactId>jakarta.ws.rs-api</artifactId> </exclusion> </exclusions> </dependency> @@ -126,7 +117,33 @@ <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> - <version>6.14.3</version> + <!-- 7.4.0 is broken, so until 7.5.0 is released ... --> + <version>7.3.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-all</artifactId> + <version>1.3</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>ziplock</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jboss.arquillian.testng</groupId> + <artifactId>arquillian-testng-container</artifactId> + <version>1.7.0.Alpha10</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>mp-common</artifactId> + <version>${project.version}</version> <scope>test</scope> </dependency> diff --git a/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKDeploymentPackager.java b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKDeploymentPackager.java index 39e390fa89..67c49a954b 100644 --- a/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKDeploymentPackager.java +++ b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKDeploymentPackager.java @@ -16,6 +16,8 @@ */ package org.apache.tomee.microprofile.tck.openapi; +import org.apache.ziplock.JarLocation; +import org.hamcrest.Matchers; import org.jboss.arquillian.container.test.spi.TestDeployment; import org.jboss.arquillian.container.test.spi.client.deployment.ProtocolArchiveProcessor; import org.jboss.arquillian.protocol.servlet5.v_5.ServletProtocolDeploymentPackager; @@ -35,10 +37,7 @@ public class MicroProfileOpenAPITCKDeploymentPackager extends ServletProtocolDep final Collection<ProtocolArchiveProcessor> processors) { final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "microprofile-openapi.war") .merge(testDeployment.getApplicationArchive()) - // TODO - This doesn't seem right. This is for the JAX-RS endpoints to be CDI scanned. - // This is to use CDI events to filter endpoints with configuration. - // Check org.apache.geronimo.microprofile.openapi.cdi.GeronimoOpenAPIExtension.findEndpointsAndApplication() - // A beans.xml should not be required. + .addAsLibrary(JarLocation.jarLocation(Matchers.class)) // required for ModelConstructionTest .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") ; diff --git a/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKExtension.java b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKExtension.java index f81544c07d..a5ecb35902 100644 --- a/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKExtension.java +++ b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenAPITCKExtension.java @@ -16,6 +16,7 @@ */ package org.apache.tomee.microprofile.tck.openapi; +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; import org.jboss.arquillian.container.test.spi.client.protocol.Protocol; import org.jboss.arquillian.core.spi.LoadableExtension; import org.jboss.arquillian.protocol.servlet5.v_5.ServletProtocol; @@ -25,6 +26,7 @@ public class MicroProfileOpenAPITCKExtension implements LoadableExtension { public void register(final ExtensionBuilder extensionBuilder) { extensionBuilder .override(Protocol.class, ServletProtocol.class, MicroProfileOpenAPITCKProtocol.class) + .service(ApplicationArchiveProcessor.class, MicroProfileOpenApiDeploymentProcessor.class) .observer(MicroProfileOpenAPITCKObserver.class) ; } diff --git a/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenApiDeploymentProcessor.java b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenApiDeploymentProcessor.java new file mode 100644 index 0000000000..2b9c9b6eb6 --- /dev/null +++ b/tck/microprofile-tck/openapi/src/test/java/org.apache.tomee.microprofile.tck.openapi/MicroProfileOpenApiDeploymentProcessor.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomee.microprofile.tck.openapi; + +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.OpenApiConfigImpl; +import io.smallrye.openapi.api.OpenApiDocument; +import io.smallrye.openapi.runtime.OpenApiProcessor; +import io.smallrye.openapi.runtime.OpenApiStaticFile; +import io.smallrye.openapi.runtime.io.Format; +import io.smallrye.openapi.runtime.scanner.FilteredIndexView; +import org.apache.tomee.microprofile.openapi.MicroProfileOpenApiEndpoint; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; +import org.jboss.arquillian.test.spi.TestClass; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.ByteArrayAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.Optional; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static io.smallrye.openapi.runtime.OpenApiProcessor.getFilter; +import static io.smallrye.openapi.runtime.OpenApiProcessor.modelFromAnnotations; +import static io.smallrye.openapi.runtime.OpenApiProcessor.modelFromReader; +import static io.smallrye.openapi.runtime.io.Format.JSON; +import static io.smallrye.openapi.runtime.io.Format.YAML; +import static io.smallrye.openapi.runtime.io.OpenApiSerializer.serialize; +import static java.lang.Thread.currentThread; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.ofNullable; + +/** + * Could probably be merged into the DeploymentProcessor. It's responsible for generating an OpenAPI document + * and add it into the archive. + */ +public class MicroProfileOpenApiDeploymentProcessor implements ApplicationArchiveProcessor { + private static Logger LOGGER = Logger.getLogger(MicroProfileOpenApiDeploymentProcessor.class.getName()); + public static volatile ClassLoader classLoader; + + @Override + public void process(Archive<?> archive, TestClass testClass) { + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + + if (archive instanceof WebArchive) { + WebArchive war = (WebArchive) archive; + + generateOpenAPI(war); + + LOGGER.log(Level.FINE, () -> war.toString(true)); + } + } + + /** + * Builds the OpenAPI file and copies it to the deployed application. + */ + private static void generateOpenAPI(final WebArchive war) { + OpenApiConfig openApiConfig = config(war); + Index index = index(war, openApiConfig); + ClassLoader contextClassLoader = currentThread().getContextClassLoader(); + + Optional<OpenAPI> annotationModel = ofNullable(modelFromAnnotations(openApiConfig, contextClassLoader, index)); + Optional<OpenAPI> readerModel = ofNullable(modelFromReader(openApiConfig, contextClassLoader)); + Optional<OpenAPI> staticFileModel = Stream.of(modelFromFile(war, "/META-INF/openapi.json", JSON), + modelFromFile(war, "/META-INF/openapi.yaml", YAML), + modelFromFile(war, "/META-INF/openapi.yml", YAML)) + .filter(Optional::isPresent) + .findFirst() + .flatMap(openAPI -> openAPI); + + OpenApiDocument document = OpenApiDocument.INSTANCE; + document.reset(); + document.config(openApiConfig); + annotationModel.ifPresent(document::modelFromAnnotations); + readerModel.ifPresent(document::modelFromReader); + staticFileModel.ifPresent(document::modelFromStaticFile); + document.filter(getFilter(openApiConfig, contextClassLoader)); + document.initialize(); + OpenAPI openAPI = document.get(); + + try { + war.addAsManifestResource(new ByteArrayAsset(serialize(openAPI, JSON).getBytes(UTF_8)), "openapi.json"); + war.addAsManifestResource(new ByteArrayAsset(serialize(openAPI, YAML).getBytes(UTF_8)), "openapi.yaml"); + } catch (IOException e) { + // Ignore + } + + document.reset(); + } + + /** + * Provides the Jandex index. + */ + private static Index index(final WebArchive war, final OpenApiConfig config) { + FilteredIndexView filteredIndexView = new FilteredIndexView(null, config); + Indexer indexer = new Indexer(); + Collection<Node> classes = war.getContent(object -> object.get().endsWith(".class")).values(); + for (Node value : classes) { + try { + String resource = value.getPath().get().replaceAll("/WEB-INF/classes/", ""); + // We remove the OpenApinEndpoint so the /openapi is not generated + if (resource.contains(MicroProfileOpenApiEndpoint.class.getSimpleName())) { + continue; + } + + DotName dotName = DotName.createSimple(resource.replaceAll("/", ".").substring(0, resource.length() - 6)); + if (filteredIndexView.accepts(dotName)) { + indexer.index(MicroProfileOpenApiDeploymentProcessor.class.getClassLoader().getResourceAsStream(resource)); + } + } catch (IOException e) { + // Ignore + } + } + return indexer.complete(); + } + + /** + * Creates the config from the microprofile-config.properties file in the application. The spec defines that the + * config file may be present in two locations. + */ + private static OpenApiConfig config(final WebArchive war) { + Optional<Node> microprofileConfig = Stream.of(ofNullable(war.get("/META-INF/microprofile-config.properties")), + ofNullable(war.get("/WEB-INF/classes/META-INF/microprofile-config.properties"))) + .filter(Optional::isPresent) + .findFirst() + .flatMap(node -> node); + + if (!microprofileConfig.isPresent()) { + return new OpenApiConfigImpl(ConfigProvider.getConfig()); + } + + Properties properties = new Properties(); + try (InputStreamReader reader = new InputStreamReader(microprofileConfig.get().getAsset().openStream(), UTF_8)) { + properties.load(reader); + } catch (IOException e) { + e.printStackTrace(); + } + + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultSources() + .addDefaultInterceptors() + .withSources(new PropertiesConfigSource(properties, "microprofile-config.properties")) + .build(); + + return new OpenApiConfigImpl(config); + } + + private static Optional<OpenAPI> modelFromFile(final WebArchive war, final String location, + final Format format) { + return ofNullable(war.get(location)) + .map(Node::getAsset) + .map(asset -> new OpenApiStaticFile(asset.openStream(), format)) + .map(OpenApiProcessor::modelFromStaticFile); + } +} diff --git a/tck/microprofile-tck/openapi/src/test/resources/arquillian.xml b/tck/microprofile-tck/openapi/src/test/resources/arquillian.xml index 669ed090fe..5fea36e73b 100644 --- a/tck/microprofile-tck/openapi/src/test/resources/arquillian.xml +++ b/tck/microprofile-tck/openapi/src/test/resources/arquillian.xml @@ -27,6 +27,7 @@ <property name="ajpPort">-1</property> <property name="stopPort">-1</property> <property name="classifier">microprofile</property> + <property name="debug">false</property> <property name="conf">src/test/conf</property> <property name="dir">target/tomee</property> <property name="appWorkingDir">target/workdir</property> diff --git a/tck/microprofile-tck/openapi/tck-dev.xml b/tck/microprofile-tck/openapi/tck-dev.xml new file mode 100644 index 0000000000..848c59732c --- /dev/null +++ b/tck/microprofile-tck/openapi/tck-dev.xml @@ -0,0 +1,25 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<suite name="microprofile-openapi-TCK" verbose="2" configfailurepolicy="continue" > + + <test name="microprofile-openapi TCK"> + <classes> + <class name="org.eclipse.microprofile.openapi.tck.ModelConstructionTest"></class> + </classes> + </test> + +</suite> \ No newline at end of file diff --git a/tck/microprofile-tck/opentracing/pom.xml b/tck/microprofile-tck/opentracing/pom.xml index a96cc70657..ff542b3525 100644 --- a/tck/microprofile-tck/opentracing/pom.xml +++ b/tck/microprofile-tck/opentracing/pom.xml @@ -118,7 +118,7 @@ <dependency> <groupId>org.apache.tomee</groupId> <artifactId>cxf-rt-rs-mp-client-shade</artifactId> - <version>9.0.0-M9-SNAPSHOT</version> + <version>${project.version}</version> <scope>test</scope> </dependency> diff --git a/tck/microprofile-tck/pom.xml b/tck/microprofile-tck/pom.xml index acaf6f6869..da59e095b7 100644 --- a/tck/microprofile-tck/pom.xml +++ b/tck/microprofile-tck/pom.xml @@ -35,10 +35,10 @@ <module>health</module> <module>metrics</module> <module>rest-client</module> + <module>openapi</module> <!-- <module>opentracing</module> <module>fault-tolerance</module> - <module>openapi</module> --> </modules> </project> diff --git a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/TomEEMicroProfileListener.java b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/TomEEMicroProfileListener.java index 33d81a9b6e..4b3502a9b0 100644 --- a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/TomEEMicroProfileListener.java +++ b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/TomEEMicroProfileListener.java @@ -28,6 +28,7 @@ import org.apache.openejb.util.Logger; import org.apache.tomee.catalina.event.AfterApplicationCreated; import org.apache.tomee.installer.Paths; import org.apache.tomee.microprofile.health.MicroProfileHealthChecksEndpoint; +import org.apache.tomee.microprofile.openapi.MicroProfileOpenApiEndpoint; import java.io.File; import java.net.MalformedURLException; @@ -105,7 +106,7 @@ public class TomEEMicroProfileListener { // These remove duplicated REST API endpoints. // webApp.restClass.removeIf(className -> className.equals(MicroProfileHealthChecksEndpoint.class.getName())); - // webApp.restClass.removeIf(className -> className.equals(MetricsEndpoints.class.getName())); + // webApp.restClass.removeIf(className -> className.equals(MicroProfileOpenApiEndpoint.class.getName())); // There remove all of MP REST API endpoint if there is a servlet already registered in /*. The issue here is // that REST path has priority over servlet and there may override old applications that have servlets diff --git a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileServletContainerInitializer.java b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileMetricsRegistration.java similarity index 83% rename from tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileServletContainerInitializer.java rename to tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileMetricsRegistration.java index 93b3956e89..6ce87a4611 100644 --- a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileServletContainerInitializer.java +++ b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/metrics/MicroProfileMetricsRegistration.java @@ -22,13 +22,6 @@ import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRegistration; -import jakarta.servlet.annotation.HandlesTypes; -import jakarta.servlet.annotation.WebFilter; -import jakarta.servlet.annotation.WebServlet; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Application; -import org.apache.tomee.microprofile.jwt.MPJWTFilter; -import org.eclipse.microprofile.auth.LoginConfig; import java.util.Set; @@ -41,7 +34,7 @@ import java.util.Set; WebFilter.class }) */ -public class MicroProfileServletContainerInitializer implements ServletContainerInitializer { +public class MicroProfileMetricsRegistration implements ServletContainerInitializer { @Override public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException { diff --git a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiEndpoint.java b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiEndpoint.java new file mode 100644 index 0000000000..09ae231b5f --- /dev/null +++ b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiEndpoint.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomee.microprofile.openapi; + +import io.smallrye.openapi.runtime.io.Format; +import jakarta.servlet.ServletContext; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.microprofile.openapi.models.OpenAPI; + +import java.io.IOException; +import java.util.Locale; +import java.util.stream.Stream; + +import static io.smallrye.openapi.runtime.io.Format.JSON; +import static io.smallrye.openapi.runtime.io.Format.YAML; +import static io.smallrye.openapi.runtime.io.OpenApiSerializer.serialize; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * This is not a JAXRS endpoint but a regular servlet because the Smallrye does the remaining job. We could do it as + * a regular REST endpoint though. + */ +@WebServlet(name = "openapi-servlet", urlPatterns = "/openapi/*") +public class MicroProfileOpenApiEndpoint extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + final ServletContext servletContext = request.getServletContext(); + final OpenAPI openAPI = MicroProfileOpenApiRegistration.getOpenApi(servletContext); + final String format = request.getParameter("format"); + final Format formatOpenApi = getOpenApiFormat(request, format); + response.setContentType(formatOpenApi.getMimeType()); + response.getOutputStream().write(serialize(openAPI, formatOpenApi).getBytes(UTF_8)); + } + + private Format getOpenApiFormat(final HttpServletRequest request, final String format) { + return Stream.of(Format.values()) + .filter(f -> format != null && f.name().compareToIgnoreCase(format) == 0) + .findFirst() + .orElse(request.getHeader("Accept").toLowerCase(Locale.ENGLISH).contains("application/json") ? JSON : YAML); + } +} diff --git a/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiRegistration.java b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiRegistration.java new file mode 100644 index 0000000000..a1e1785e6d --- /dev/null +++ b/tomee/tomee-microprofile/mp-common/src/main/java/org/apache/tomee/microprofile/openapi/MicroProfileOpenApiRegistration.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomee.microprofile.openapi; + +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.OpenApiConfigImpl; +import io.smallrye.openapi.api.OpenApiDocument; +import io.smallrye.openapi.runtime.OpenApiProcessor; +import io.smallrye.openapi.runtime.OpenApiStaticFile; +import io.smallrye.openapi.runtime.io.Format; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.openapi.models.OpenAPI; + +import java.net.URL; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static io.smallrye.openapi.runtime.io.Format.JSON; +import static io.smallrye.openapi.runtime.io.Format.YAML; + +/** + * Responsible for adding the filter into the chain and doing all other initialization + * <p> + * todo do we want to be so restrictive with the HandlesTypes annotation? + * + * @HandlesTypes({Path.class, WebServlet.class, + * WebFilter.class + * }) + */ +public class MicroProfileOpenApiRegistration implements ServletContainerInitializer { + + @Override + public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException { + final ServletRegistration.Dynamic servletRegistration = + ctx.addServlet("mp-openapi-servlet", MicroProfileOpenApiEndpoint.class); + servletRegistration.addMapping("/openapi/*"); + + openApi(ctx); + } + + private void openApi(final ServletContext servletContext) { + try { + Optional<OpenAPI> staticOpenApi = Stream + .of(readOpenApiFile(servletContext, "/META-INF/openapi.json", JSON), + readOpenApiFile(servletContext, "/META-INF/openapi.yaml", YAML), + readOpenApiFile(servletContext, "/META-INF/openapi.yml", YAML)) + .filter(Optional::isPresent) + .findFirst() + .flatMap(file -> file); + + staticOpenApi.ifPresent(openAPI -> setOpenApi(servletContext,openAPI)); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void setOpenApi(final ServletContext servletContext, final OpenAPI openAPI) { + Objects.requireNonNull(servletContext); + Objects.requireNonNull(openAPI); + servletContext.setAttribute(MicroProfileOpenApiRegistration.class.getName() + ".OpenAPI", openAPI); + } + + public static OpenAPI getOpenApi(final ServletContext servletContext) { + Objects.requireNonNull(servletContext); + return (OpenAPI) servletContext.getAttribute(MicroProfileOpenApiRegistration.class.getName() + ".OpenAPI"); + } + + private Optional<OpenAPI> readOpenApiFile( + final ServletContext servletContext, + final String location, + final Format format) + throws Exception { + + final URL resource = servletContext.getResource(location); + if (resource == null) { + return Optional.empty(); + } + + final OpenApiDocument document = OpenApiDocument.INSTANCE; + try (OpenApiStaticFile staticFile = new OpenApiStaticFile(resource.openStream(), format)) { + Config config = ConfigProvider.getConfig(); + OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); + document.reset(); + document.config(openApiConfig); + document.filter(OpenApiProcessor.getFilter(openApiConfig, Thread.currentThread().getContextClassLoader())); + document.modelFromStaticFile(OpenApiProcessor.modelFromStaticFile(staticFile)); + document.initialize(); + return Optional.of(document.get()); + } finally { + document.reset(); + } + } + +} \ No newline at end of file diff --git a/tomee/tomee-microprofile/mp-common/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/tomee/tomee-microprofile/mp-common/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer index 5d63c21c21..ccc38a2e9c 100644 --- a/tomee/tomee-microprofile/mp-common/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer +++ b/tomee/tomee-microprofile/mp-common/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -1 +1,2 @@ -org.apache.tomee.microprofile.metrics.MicroProfileServletContainerInitializer \ No newline at end of file +org.apache.tomee.microprofile.metrics.MicroProfileMetricsRegistration +org.apache.tomee.microprofile.openapi.MicroProfileOpenApiRegistration \ No newline at end of file
