You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by jg...@apache.org on 2023/11/08 10:00:09 UTC

(tomee) 02/02: feat(#TOMEE-4268): MicroProfile OpenAPI Reader example.

This is an automated email from the ASF dual-hosted git repository.

jgallimore pushed a commit to branch tomee-9.x
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit 464d7e04bac1a376e3f8e8621edb6165764f6c59
Author: Jean-Louis Monteiro <jl...@tomitribe.com>
AuthorDate: Mon Oct 30 15:29:04 2023 +0100

    feat(#TOMEE-4268): MicroProfile OpenAPI Reader example.
---
 examples/mp-openapi-reader/pom.xml                 | 127 +++++++++++++++++++
 .../microprofile/openapi/CustomReader.java         |  77 ++++++++++++
 .../microprofile/openapi/WeatherService.java       |  37 ++++++
 .../microprofile/openapi/moviefun/Movie.java       | 102 ++++++++++++++++
 .../microprofile/openapi/moviefun/MoviesBean.java  |  92 ++++++++++++++
 .../openapi/moviefun/rest/ApplicationConfig.java   |  34 ++++++
 .../openapi/moviefun/rest/LoadRest.java            |  41 +++++++
 .../openapi/moviefun/rest/MoviesRest.java          |  82 +++++++++++++
 .../META-INF/microprofile-config.properties        |   2 +
 .../src/main/resources/META-INF/persistence.xml    |  31 +++++
 .../jakarta.servlet.ServletContainerInitializer    |   1 +
 .../src/main/webapp/WEB-INF/web.xml                |  25 ++++
 .../src/main/webapp/my-openapi.json                |  25 ++++
 .../src/main/webapp/openapi/openapi.yaml           | 135 +++++++++++++++++++++
 .../src/test/java/WeatherServiceTest.java          |  95 +++++++++++++++
 .../src/test/resources/arquillian.xml              |  31 +++++
 examples/pom.xml                                   |   1 +
 17 files changed, 938 insertions(+)

diff --git a/examples/mp-openapi-reader/pom.xml b/examples/mp-openapi-reader/pom.xml
new file mode 100644
index 0000000000..95ae07fc7e
--- /dev/null
+++ b/examples/mp-openapi-reader/pom.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.superbiz</groupId>
+  <artifactId>mp-openapi-reader</artifactId>
+  <version>10.0.0-SNAPSHOT</version>
+  <packaging>war</packaging>
+  <name>TomEE :: Examples :: Microprofile OpenAPI Reader</name>
+  <properties>
+    <version.jakartaee-api>9.1.1</version.jakartaee-api>
+    <version.arquillian.bom>1.7.0.Final</version.arquillian.bom>
+    <junit.version>4.13.2</junit.version>
+    <tomee.version>10.0.0-SNAPSHOT</tomee.version>
+    <version.microprofile.impl.openapi>3.0.1</version.microprofile.impl.openapi>
+    <version.openapi-api>3.1.1</version.openapi-api>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>jakartaee-api</artifactId>
+      <version>${version.jakartaee-api}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.microprofile.openapi</groupId>
+      <artifactId>microprofile-openapi-api</artifactId>
+      <version>${version.openapi-api}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.smallrye</groupId>
+      <artifactId>smallrye-open-api</artifactId>
+      <version>${version.microprofile.impl.openapi}</version>
+      <scope>provided</scope>
+      <type>pom</type>
+    </dependency>
+    <dependency>
+      <groupId>io.smallrye</groupId>
+      <artifactId>smallrye-open-api-core</artifactId>
+      <version>${version.microprofile.impl.openapi}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>${junit.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>openejb-cxf-rs</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jboss.arquillian.junit</groupId>
+      <artifactId>arquillian-junit-container</artifactId>
+      <version>${version.arquillian.bom}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>arquillian-tomee-remote</artifactId>
+      <version>${tomee.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomee</groupId>
+      <artifactId>apache-tomee</artifactId>
+      <version>${tomee.version}</version>
+      <type>zip</type>
+      <classifier>microprofile</classifier>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.tomee.maven</groupId>
+        <artifactId>tomee-maven-plugin</artifactId>
+        <version>${tomee.version}</version>
+        <configuration>
+          <tomeeClassifier>microprofile</tomeeClassifier>
+          <context>${project.artifactId}</context>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.7.0</version>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+  <!-- This section allows you to configure where to publish libraries for
+  sharing. It is not required and may be deleted. For more information see:
+  http://maven.apache.org/plugins/maven-deploy-plugin/ -->
+  <distributionManagement>
+    <repository>
+      <id>localhost</id>
+      <url>file://${basedir}/target/repo/</url>
+    </repository>
+    <snapshotRepository>
+      <id>localhost</id>
+      <url>file://${basedir}/target/snapshot-repo/</url>
+    </snapshotRepository>
+  </distributionManagement>
+</project>
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/CustomReader.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/CustomReader.java
new file mode 100644
index 0000000000..2dc481d838
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/CustomReader.java
@@ -0,0 +1,77 @@
+package org.superbiz.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 org.eclipse.microprofile.config.ConfigProvider;
+import org.eclipse.microprofile.openapi.OASModelReader;
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+
+import java.net.URL;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import static io.smallrye.openapi.runtime.io.Format.JSON;
+import static io.smallrye.openapi.runtime.io.Format.YAML;
+
+public class CustomReader implements OASModelReader, ServletContainerInitializer {
+
+    private static final Logger LOGGER = Logger.getLogger(CustomReader.class.getName());
+
+    private static Optional<OpenAPI> openAPI;
+
+    @Override
+    public OpenAPI buildModel() {
+        return openAPI.orElseThrow(() -> new IllegalStateException("OpenAPI document not available."));
+    }
+
+    @Override
+    public void onStartup(final Set<Class<?>> c, final ServletContext ctx) throws ServletException {
+        try {
+            final OpenApiConfig openApiConfig = new OpenApiConfigImpl(ConfigProvider.getConfig());;
+            final Optional<OpenAPI> yaml = readOpenApiFile(ctx, "/WEB-INF/classes/openapi/openapi.yaml", YAML);
+            final Optional<OpenAPI> json = readOpenApiFile(ctx, "/WEB-INF/classes/my-openapi.json", JSON);
+
+            final OpenApiDocument document = OpenApiDocument.INSTANCE;
+            try {
+                document.reset();
+                document.config(openApiConfig);
+                yaml.ifPresent(document::modelFromStaticFile);
+                json.ifPresent(document::modelFromStaticFile);
+                document.initialize();
+
+                openAPI = Optional.ofNullable(document.get());
+
+            } finally {
+                document.reset();
+            }
+
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static Optional<OpenAPI> readOpenApiFile(
+        final ServletContext servletContext, final String location,
+        final Format format) throws Exception {
+
+        final URL resource = servletContext.getResource(location);
+        if (resource == null) {
+            LOGGER.fine("Could not find static OpenAPI file " + location);
+            return Optional.empty();
+        }
+
+        LOGGER.fine("Found static OpenAPI file " + location);
+
+        try (OpenApiStaticFile staticFile = new OpenApiStaticFile(resource.openStream(), format)) {
+            return Optional.of(OpenApiProcessor.modelFromStaticFile(staticFile));
+        }
+    }
+}
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/WeatherService.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/WeatherService.java
new file mode 100644
index 0000000000..bc6df1889d
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/WeatherService.java
@@ -0,0 +1,37 @@
+package org.superbiz.microprofile.openapi; /**
+ * 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.
+ */
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/weather")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@ApplicationScoped
+public class WeatherService {
+
+    @Path("/day/status")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public String dayStatus() {
+        return "Hi, today is a sunny day!";
+    }
+}
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/Movie.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/Movie.java
new file mode 100644
index 0000000000..5bf8264317
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/Movie.java
@@ -0,0 +1,102 @@
+/**
+ * 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.superbiz.microprofile.openapi.moviefun;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+@Entity
+@XmlRootElement(name = "movie")
+public class Movie {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    private long id;
+
+    private String director;
+    private String title;
+    private int year;
+    private String genre;
+    private int rating;
+
+    public Movie() {
+    }
+
+    public Movie(String title, String director, String genre, int rating, int year) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+        this.genre = genre;
+        this.rating = rating;
+    }
+
+    public Movie(String director, String title, int year) {
+        this.director = director;
+        this.title = title;
+        this.year = year;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public String getDirector() {
+        return director;
+    }
+
+    public void setDirector(String director) {
+        this.director = director;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public int getYear() {
+        return year;
+    }
+
+    public void setYear(int year) {
+        this.year = year;
+    }
+
+    public String getGenre() {
+        return genre;
+    }
+
+    public void setGenre(String genre) {
+        this.genre = genre;
+    }
+
+    public int getRating() {
+        return rating;
+    }
+
+    public void setRating(int rating) {
+        this.rating = rating;
+    }
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/MoviesBean.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/MoviesBean.java
new file mode 100644
index 0000000000..803e53f40c
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/MoviesBean.java
@@ -0,0 +1,92 @@
+/**
+ * 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.superbiz.microprofile.openapi.moviefun;
+
+import jakarta.ejb.Stateless;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Path;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import jakarta.persistence.metamodel.EntityType;
+
+import java.util.List;
+
+@Stateless
+public class MoviesBean {
+
+    @PersistenceContext(unitName = "movie-unit")
+    private EntityManager entityManager;
+
+    public Movie find(Long id) {
+        return entityManager.find(Movie.class, id);
+    }
+
+    public void addMovie(Movie movie) {
+        entityManager.persist(movie);
+    }
+
+    public void editMovie(Movie movie) {
+        entityManager.merge(movie);
+    }
+
+    public void deleteMovie(long id) {
+        Movie movie = entityManager.find(Movie.class, id);
+        entityManager.remove(movie);
+    }
+
+    public List<Movie> getMovies(Integer firstResult, Integer maxResults, String field, String searchTerm) {
+        CriteriaBuilder qb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Movie> cq = qb.createQuery(Movie.class);
+        Root<Movie> root = cq.from(Movie.class);
+        EntityType<Movie> type = entityManager.getMetamodel().entity(Movie.class);
+        if (field != null && searchTerm != null && !"".equals(field.trim()) && !"".equals(searchTerm.trim())) {
+            Path<String> path = root.get(type.getDeclaredSingularAttribute(field.trim(), String.class));
+            Predicate condition = qb.like(path, "%" + searchTerm.trim() + "%");
+            cq.where(condition);
+        }
+        TypedQuery<Movie> q = entityManager.createQuery(cq);
+        if (maxResults != null) {
+            q.setMaxResults(maxResults);
+        }
+        if (firstResult != null) {
+            q.setFirstResult(firstResult);
+        }
+        return q.getResultList();
+    }
+
+    public int count(String field, String searchTerm) {
+        CriteriaBuilder qb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Long> cq = qb.createQuery(Long.class);
+        Root<Movie> root = cq.from(Movie.class);
+        EntityType<Movie> type = entityManager.getMetamodel().entity(Movie.class);
+        cq.select(qb.count(root));
+        if (field != null && searchTerm != null && !"".equals(field.trim()) && !"".equals(searchTerm.trim())) {
+            Path<String> path = root.get(type.getDeclaredSingularAttribute(field.trim(), String.class));
+            Predicate condition = qb.like(path, "%" + searchTerm.trim() + "%");
+            cq.where(condition);
+        }
+        return entityManager.createQuery(cq).getSingleResult().intValue();
+    }
+
+    public void clean() {
+        entityManager.createQuery("delete from Movie").executeUpdate();
+    }
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/ApplicationConfig.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/ApplicationConfig.java
new file mode 100644
index 0000000000..625515c3fe
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/ApplicationConfig.java
@@ -0,0 +1,34 @@
+/**
+ * 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.superbiz.microprofile.openapi.moviefun.rest;
+
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@ApplicationPath("/rest")
+public class ApplicationConfig extends Application {
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Set<Class<?>> getClasses() {
+        return new HashSet<Class<?>>(Arrays.asList(LoadRest.class, MoviesRest.class));
+    }
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/LoadRest.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/LoadRest.java
new file mode 100644
index 0000000000..2d5d1e6e67
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/LoadRest.java
@@ -0,0 +1,41 @@
+/**
+ * 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.superbiz.microprofile.openapi.moviefun.rest;
+
+import jakarta.ejb.EJB;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import org.superbiz.microprofile.openapi.moviefun.Movie;
+import org.superbiz.microprofile.openapi.moviefun.MoviesBean;
+
+@Path("load")
+public class LoadRest {
+    @EJB
+    private MoviesBean moviesBean;
+
+    @POST
+    public void load() {
+        moviesBean.addMovie(new Movie("Wedding Crashers", "David Dobkin", "Comedy", 7, 2005));
+        moviesBean.addMovie(new Movie("Starsky & Hutch", "Todd Phillips", "Action", 6, 2004));
+        moviesBean.addMovie(new Movie("Shanghai Knights", "David Dobkin", "Action", 6, 2003));
+        moviesBean.addMovie(new Movie("I-Spy", "Betty Thomas", "Adventure", 5, 2002));
+        moviesBean.addMovie(new Movie("The Royal Tenenbaums", "Wes Anderson", "Comedy", 8, 2001));
+        moviesBean.addMovie(new Movie("Zoolander", "Ben Stiller", "Comedy", 6, 2001));
+        moviesBean.addMovie(new Movie("Shanghai Noon", "Tom Dey", "Comedy", 7, 2000));
+    }
+
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/MoviesRest.java b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/MoviesRest.java
new file mode 100644
index 0000000000..80d2e889bf
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/java/org/superbiz/microprofile/openapi/moviefun/rest/MoviesRest.java
@@ -0,0 +1,82 @@
+/**
+ * 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.superbiz.microprofile.openapi.moviefun.rest;
+
+import jakarta.ejb.EJB;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import org.superbiz.microprofile.openapi.moviefun.Movie;
+import org.superbiz.microprofile.openapi.moviefun.MoviesBean;
+
+import java.util.List;
+
+@Path("movies")
+@Produces({"application/json"})
+public class MoviesRest {
+
+    @EJB
+    private MoviesBean service;
+
+    @GET
+    @Path("{id}")
+    public Movie find(@PathParam("id") Long id) {
+        return service.find(id);
+    }
+
+    @GET
+    public List<Movie> getMovies(@QueryParam("first") Integer first, @QueryParam("max") Integer max,
+                                 @QueryParam("field") String field, @QueryParam("searchTerm") String searchTerm) {
+        return service.getMovies(first, max, field, searchTerm);
+    }
+
+    @POST
+    @Consumes("application/json")
+    public Movie addMovie(Movie movie) {
+        service.addMovie(movie);
+        return movie;
+    }
+
+    @PUT
+    @Path("{id}")
+    @Consumes("application/json")
+    public Movie editMovie(Movie movie) {
+        service.editMovie(movie);
+        return movie;
+    }
+
+    @DELETE
+    @Path("{id}")
+    public void deleteMovie(@PathParam("id") long id) {
+        service.deleteMovie(id);
+    }
+
+    @GET
+    @Path("count")
+    @Produces(MediaType.TEXT_PLAIN)
+    public int count(@QueryParam("field") String field, @QueryParam("searchTerm") String searchTerm) {
+        return service.count(field, searchTerm);
+    }
+
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/resources/META-INF/microprofile-config.properties b/examples/mp-openapi-reader/src/main/resources/META-INF/microprofile-config.properties
new file mode 100644
index 0000000000..268e2ad12c
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/resources/META-INF/microprofile-config.properties
@@ -0,0 +1,2 @@
+mp.openapi.model.reader=org.superbiz.microprofile.openapi.CustomReader
+mp.openapi.scan.disable=false
diff --git a/examples/mp-openapi-reader/src/main/resources/META-INF/persistence.xml b/examples/mp-openapi-reader/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000000..ec38aaa117
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+-->
+<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
+             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
+  <persistence-unit name="movie-unit">
+    <jta-data-source>movieDatabase</jta-data-source>
+    <non-jta-data-source>movieDatabaseUnmanaged</non-jta-data-source>
+    <class>org.superbiz.moviefun.Movie</class>
+
+    <properties>
+      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
+    </properties>
+  </persistence-unit>
+</persistence>
diff --git a/examples/mp-openapi-reader/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/examples/mp-openapi-reader/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
new file mode 100644
index 0000000000..d0eefa40f3
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
@@ -0,0 +1 @@
+org.superbiz.microprofile.openapi.CustomReader
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/webapp/WEB-INF/web.xml b/examples/mp-openapi-reader/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..6e1611111f
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     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.
+  -->
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         metadata-complete="false"
+         version="2.5">
+
+  <display-name>Microprofile OpenAPI Reader</display-name>
+</web-app>
diff --git a/examples/mp-openapi-reader/src/main/webapp/my-openapi.json b/examples/mp-openapi-reader/src/main/webapp/my-openapi.json
new file mode 100644
index 0000000000..21cefed376
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/webapp/my-openapi.json
@@ -0,0 +1,25 @@
+{
+  "openapi" : "3.0.3",
+  "info" : {
+    "title" : "Generated API",
+    "version" : "1.0"
+  },
+  "paths" : {
+    "/rest/weather/day/status" : {
+      "get" : {
+        "responses" : {
+          "200" : {
+            "description" : "OK",
+            "content" : {
+              "text/plain" : {
+                "schema" : {
+                  "type" : "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/main/webapp/openapi/openapi.yaml b/examples/mp-openapi-reader/src/main/webapp/openapi/openapi.yaml
new file mode 100644
index 0000000000..53fb4a7fe9
--- /dev/null
+++ b/examples/mp-openapi-reader/src/main/webapp/openapi/openapi.yaml
@@ -0,0 +1,135 @@
+---
+openapi: 3.0.3
+info:
+  title: Generated API
+  version: "1.0"
+paths:
+  /rest/load:
+    post:
+      responses:
+        "201":
+          description: Created
+  /rest/movies:
+    get:
+      parameters:
+        - name: field
+          in: query
+          schema:
+            type: string
+        - name: first
+          in: query
+          schema:
+            format: int32
+            type: integer
+        - name: max
+          in: query
+          schema:
+            format: int32
+            type: integer
+        - name: searchTerm
+          in: query
+          schema:
+            type: string
+      responses:
+        "200":
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Movie'
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Movie'
+      responses:
+        "200":
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Movie'
+  /rest/movies/count:
+    get:
+      parameters:
+        - name: field
+          in: query
+          schema:
+            type: string
+        - name: searchTerm
+          in: query
+          schema:
+            type: string
+      responses:
+        "200":
+          description: OK
+          content:
+            text/plain:
+              schema:
+                format: int32
+                type: integer
+  /rest/movies/{id}:
+    get:
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema:
+            format: int64
+            type: integer
+      responses:
+        "200":
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Movie'
+    put:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Movie'
+      responses:
+        "200":
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Movie'
+    delete:
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema:
+            format: int64
+            type: integer
+      responses:
+        "204":
+          description: No Content
+components:
+  schemas:
+    Movie:
+      type: object
+      properties:
+        id:
+          format: int64
+          type: integer
+        director:
+          type: string
+        title:
+          type: string
+        year:
+          format: int32
+          type: integer
+        genre:
+          type: string
+        rating:
+          format: int32
+          type: integer
+      xml:
+        name: movie
\ No newline at end of file
diff --git a/examples/mp-openapi-reader/src/test/java/WeatherServiceTest.java b/examples/mp-openapi-reader/src/test/java/WeatherServiceTest.java
new file mode 100644
index 0000000000..01e717c799
--- /dev/null
+++ b/examples/mp-openapi-reader/src/test/java/WeatherServiceTest.java
@@ -0,0 +1,95 @@
+/**
+ * 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.
+ */
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+import org.apache.openejb.loader.IO;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.superbiz.microprofile.openapi.CustomReader;
+import org.superbiz.microprofile.openapi.WeatherService;
+import org.superbiz.microprofile.openapi.moviefun.Movie;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+@RunWith(Arquillian.class)
+public class WeatherServiceTest {
+
+    @Deployment(testable = false)
+    public static WebArchive createDeployment() {
+        final WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "test.war")
+                .addClass(WeatherService.class)
+                .addClass(CustomReader.class)
+                .addPackages(true, Movie.class.getPackage())
+                .addAsResource("META-INF/services/jakarta.servlet.ServletContainerInitializer")
+                .addAsResource("META-INF/persistence.xml")
+                .addAsResource("META-INF/microprofile-config.properties")
+                .addAsResource(new File("src/main/webapp/my-openapi.json"), "my-openapi.json")
+                .addAsResource(new File("src/main/webapp/openapi/openapi.yaml"), "openapi/openapi.yaml")
+                .addAsWebInfResource(new StringAsset("<beans/>"), "beans.xml");
+        return webArchive;
+    }
+
+    @ArquillianResource
+    private URL base;
+
+    private Client client;
+
+    @Before
+    public void before() {
+        this.client = ClientBuilder.newClient();
+    }
+
+    @After
+    public void after() {
+        this.client.close();
+    }
+
+    @Test
+    public void openapi() throws IOException {
+        final WebTarget webTarget = client.target(base.toExternalForm());
+
+        final Response response = webTarget.path("/openapi")
+                                           .request()
+                                           .accept("application/json")
+                                           .buildGet().invoke();
+        final InputStream entity = (InputStream) response.getEntity();
+        final String payload = IO.slurp(entity);
+
+        // assert that it returns a payload
+        Assert.assertNotNull(payload);
+
+        // now check if the 2 openapi documents (yaml and json) got successfully loaded and merged
+        Assert.assertTrue(payload.contains("/rest/weather/day/status")); // from one openapi file
+        Assert.assertTrue(payload.contains("/rest/movies")); // from the other one
+    }
+
+}
diff --git a/examples/mp-openapi-reader/src/test/resources/arquillian.xml b/examples/mp-openapi-reader/src/test/resources/arquillian.xml
new file mode 100644
index 0000000000..0b54540a34
--- /dev/null
+++ b/examples/mp-openapi-reader/src/test/resources/arquillian.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+
+    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.
+-->
+<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+  <container qualifier="tomee" default="true">
+    <configuration>
+      <property name="httpPort">-1</property>
+      <property name="stopPort">-1</property>
+      <property name="debug">false</property>
+      <property name="classifier">microprofile</property>
+      <property name="dir">target/apache-tomee-remote</property>
+      <property name="appWorkingDir">target/arquillian-test-working-dir</property>
+    </configuration>
+  </container>
+</arquillian>
\ No newline at end of file
diff --git a/examples/pom.xml b/examples/pom.xml
index f126a11a8e..b5af5082d3 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -129,6 +129,7 @@
     <module>mp-rest-jwt-public-key</module>
     <module>mp-rest-client</module>
     <module>mp-custom-healthcheck</module>
+    <module>mp-openapi-reader</module>
     <module>mtom</module>
     <module>multi-jpa-provider-testing</module>
     <module>multiple-arquillian-adapters</module>