diff --git a/bom/application/pom.xml b/bom/application/pom.xml index c078597d887f5..fd494a0867252 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -55,7 +55,7 @@ 3.9.1 4.1.0 4.0.0 - 3.10.0 + 3.12.0 2.10.0 6.4.0 4.6.0 diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtension.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtension.java index a767883a999ff..611fd9cb23122 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtension.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtension.java @@ -15,13 +15,10 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; -import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner; - /** * This adds support for the quarkus.http.root-path config option */ -public class CustomPathExtension implements AnnotationScannerExtension { +public class CustomPathExtension { static final Set APPLICATION_PATH = new TreeSet<>(Arrays.asList( DotName.createSimple("jakarta.ws.rs.ApplicationPath"), @@ -35,8 +32,7 @@ public CustomPathExtension(String rootPath, String appPath) { this.appPath = appPath; } - @Override - public void processScannerApplications(AnnotationScanner scanner, Collection applications) { + public String resolveContextRoot(Collection applications) { Optional appPathAnnotationValue = applications.stream() .flatMap(app -> APPLICATION_PATH.stream().map(app::declaredAnnotation)) .filter(Objects::nonNull) @@ -48,15 +44,13 @@ public void processScannerApplications(AnnotationScanner scanner, Collection buildContextRpot(rootPath)) - .orElseGet(() -> buildContextRpot(rootPath, this.appPath)); + String contextRoot = appPathAnnotationValue.map(path -> buildContextRoot(rootPath)) + .orElseGet(() -> buildContextRoot(rootPath, this.appPath)); - if (!"/".equals(contextRoot)) { - scanner.setContextRoot(contextRoot); - } + return "/".equals(contextRoot) ? null : contextRoot; } - static String buildContextRpot(String... segments) { + static String buildContextRoot(String... segments) { String path = Stream.of(segments) .filter(Objects::nonNull) .map(CustomPathExtension::stripSlashes) diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/MediaTypeConfigSource.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/MediaTypeConfigSource.java new file mode 100644 index 0000000000000..ee27703b5c534 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/MediaTypeConfigSource.java @@ -0,0 +1,39 @@ +package io.quarkus.smallrye.openapi.deployment; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +import io.smallrye.openapi.api.SmallRyeOASConfig; + +public class MediaTypeConfigSource implements ConfigSource { + + private final Map mediaTypes = new HashMap<>(); + + public MediaTypeConfigSource() { + mediaTypes.put(SmallRyeOASConfig.DEFAULT_PRODUCES_STREAMING, "application/octet-stream"); + mediaTypes.put(SmallRyeOASConfig.DEFAULT_CONSUMES_STREAMING, "application/octet-stream"); + mediaTypes.put(SmallRyeOASConfig.DEFAULT_PRODUCES, "application/json"); + mediaTypes.put(SmallRyeOASConfig.DEFAULT_CONSUMES, "application/json"); + mediaTypes.put(SmallRyeOASConfig.DEFAULT_PRODUCES_PRIMITIVES, "text/plain"); + mediaTypes.put(SmallRyeOASConfig.DEFAULT_CONSUMES_PRIMITIVES, "text/plain"); + } + + @Override + public Set getPropertyNames() { + return mediaTypes.keySet(); + } + + @Override + public String getValue(String propertyName) { + return mediaTypes.get(propertyName); + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/RESTEasyExtension.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/RESTEasyExtension.java index ee71c46f19bd9..29a9ba9d28738 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/RESTEasyExtension.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/RESTEasyExtension.java @@ -12,9 +12,8 @@ import org.jboss.jandex.Type; import io.quarkus.deployment.util.ServiceUtil; -import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; -public class RESTEasyExtension implements AnnotationScannerExtension { +public class RESTEasyExtension { private static final DotName DOTNAME_PROVIDER = DotName.createSimple("jakarta.ws.rs.ext.Provider"); private static final DotName DOTNAME_ASYNC_RESPONSE_PROVIDER = DotName @@ -90,7 +89,6 @@ private void scanAsyncResponseProviders(IndexView index) { } } - @Override public Type resolveAsyncType(Type type) { if (type.kind() == Type.Kind.PARAMETERIZED_TYPE && asyncTypes.contains(type.name())) { diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index c7154a5b83acd..a068cc44304cd 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -8,7 +8,6 @@ import java.io.UncheckedIOException; import java.lang.reflect.Modifier; import java.net.URL; -import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -16,10 +15,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; -import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -27,12 +24,18 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -42,7 +45,6 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; -import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.spi.OASFactoryResolver; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -94,6 +96,7 @@ import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoTagFilter; import io.quarkus.smallrye.openapi.deployment.filter.ClassAndMethod; +import io.quarkus.smallrye.openapi.deployment.filter.DefaultInfoFilter; import io.quarkus.smallrye.openapi.deployment.filter.SecurityConfigFilter; import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem; import io.quarkus.smallrye.openapi.deployment.spi.IgnoreStaticDocumentBuildItem; @@ -115,19 +118,12 @@ import io.quarkus.vertx.http.deployment.spi.RouteBuildItem; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.smallrye.openapi.api.OpenApiConfig; -import io.smallrye.openapi.api.OpenApiConfigImpl; import io.smallrye.openapi.api.OpenApiDocument; +import io.smallrye.openapi.api.SmallRyeOpenAPI; import io.smallrye.openapi.api.constants.SecurityConstants; -import io.smallrye.openapi.api.models.OpenAPIImpl; import io.smallrye.openapi.api.util.MergeUtil; import io.smallrye.openapi.jaxrs.JaxRsConstants; -import io.smallrye.openapi.runtime.OpenApiProcessor; -import io.smallrye.openapi.runtime.OpenApiStaticFile; -import io.smallrye.openapi.runtime.io.Format; -import io.smallrye.openapi.runtime.io.OpenApiSerializer; -import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; import io.smallrye.openapi.runtime.scanner.FilteredIndexView; -import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner; import io.smallrye.openapi.runtime.util.JandexUtil; import io.smallrye.openapi.spring.SpringConstants; import io.smallrye.openapi.vertx.VertxConstants; @@ -139,16 +135,13 @@ * The main OpenAPI Processor. This will scan for JAX-RS, Spring and Vert.x Annotations, and, if any, add supplied schemas. * The result is added to the deployable unit to be loaded at runtime. */ +@SuppressWarnings("deprecation") public class SmallRyeOpenApiProcessor { private static final Logger log = Logger.getLogger("io.quarkus.smallrye.openapi"); - private static final String META_INF_OPENAPI_YAML = "META-INF/openapi.yaml"; - private static final String WEB_INF_CLASSES_META_INF_OPENAPI_YAML = "WEB-INF/classes/META-INF/openapi.yaml"; - private static final String META_INF_OPENAPI_YML = "META-INF/openapi.yml"; - private static final String WEB_INF_CLASSES_META_INF_OPENAPI_YML = "WEB-INF/classes/META-INF/openapi.yml"; - private static final String META_INF_OPENAPI_JSON = "META-INF/openapi.json"; - private static final String WEB_INF_CLASSES_META_INF_OPENAPI_JSON = "WEB-INF/classes/META-INF/openapi.json"; + private static final String META_INF_OPENAPI = "META-INF/openapi."; + private static final String WEB_INF_CLASSES_META_INF_OPENAPI = "WEB-INF/classes/META-INF/openapi."; private static final DotName OPENAPI_SCHEMA = DotName.createSimple(Schema.class.getName()); private static final DotName OPENAPI_RESPONSE = DotName.createSimple(APIResponse.class.getName()); @@ -168,17 +161,6 @@ public class SmallRyeOpenApiProcessor { private static final String MANAGEMENT_ENABLED = "quarkus.smallrye-openapi.management.enabled"; - static { - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_PRODUCES_STREAMING, - "application/octet-stream"); - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_CONSUMES_STREAMING, - "application/octet-stream"); - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_PRODUCES, "application/json"); - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_CONSUMES, "application/json"); - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_PRODUCES_PRIMITIVES, "text/plain"); - System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_CONSUMES_PRIMITIVES, "text/plain"); - } - @BuildStep void contributeClassesToIndex(BuildProducer additionalIndexedClasses) { // contribute additional JDK classes to the index, because SmallRye OpenAPI will check if some @@ -200,7 +182,7 @@ void registerNativeImageResources(BuildProducer servic void configFiles(BuildProducer watchedFiles, SmallRyeOpenApiConfig openApiConfig, LaunchModeBuildItem launchMode, - OutputTargetBuildItem outputTargetBuildItem) throws IOException { + OutputTargetBuildItem outputTargetBuildItem) { // Add any additional directories if configured if (launchMode.getLaunchMode().isDevOrTest() && openApiConfig.additionalDocsDirectory.isPresent()) { List additionalStaticDocuments = openApiConfig.additionalDocsDirectory.get(); @@ -213,12 +195,10 @@ void configFiles(BuildProducer watchedFiles, } } - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(META_INF_OPENAPI_YAML)); - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(WEB_INF_CLASSES_META_INF_OPENAPI_YAML)); - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(META_INF_OPENAPI_YML)); - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(WEB_INF_CLASSES_META_INF_OPENAPI_YML)); - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(META_INF_OPENAPI_JSON)); - watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(WEB_INF_CLASSES_META_INF_OPENAPI_JSON)); + Stream.of("json", "yaml", "yml").forEach(ext -> { + watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(META_INF_OPENAPI + ext)); + watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(WEB_INF_CLASSES_META_INF_OPENAPI + ext)); + }); } @BuildStep @@ -254,9 +234,8 @@ void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig, + List userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(config, apiFilteredIndexViewBuildItem.getIndex()); syntheticBeans.produce(SyntheticBeanBuildItem.configure(OpenApiRecorder.UserDefinedRuntimeFilters.class) @@ -462,13 +441,10 @@ private List getUserDefinedBuildtimeFilters(IndexView index) { return getUserDefinedFilters(index, OpenApiFilter.RunStage.BUILD); } - private List getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, IndexView index) { + private List getUserDefinedRuntimeFilters(Config config, IndexView index) { List userDefinedFilters = getUserDefinedFilters(index, OpenApiFilter.RunStage.RUN); // Also add the MP way - String filter = openApiConfig.filter(); - if (filter != null) { - userDefinedFilters.add(filter); - } + config.getOptionalValue(OASConfig.FILTER, String.class).ifPresent(userDefinedFilters::add); return userDefinedFilters; } @@ -616,9 +592,7 @@ private Map> getRolesAllowedMethodReferences(OpenApiFiltere .stream() .map(index::getAnnotations) .flatMap(Collection::stream) - .flatMap((t) -> { - return getMethods(t, index); - }) + .flatMap(t -> getMethods(t, index)) .collect(Collectors.toMap( e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey()), e -> List.of(e.getValue().value().asStringArray()), @@ -639,9 +613,7 @@ private List getPermissionsAllowedMethodReferences( return index .getAnnotations(DotName.createSimple(PermissionsAllowed.class)) .stream() - .flatMap((t) -> { - return getMethods(t, index); - }) + .flatMap(t -> getMethods(t, index)) .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); @@ -652,9 +624,7 @@ private List getAuthenticatedMethodReferences(OpenApiFilteredIndexViewBu return index .getAnnotations(DotName.createSimple(Authenticated.class.getName())) .stream() - .flatMap((t) -> { - return getMethods(t, index); - }) + .flatMap(t -> getMethods(t, index)) .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); @@ -864,43 +834,98 @@ public void build(BuildProducer feature, Capabilities capabilities, List openAPIBuildItems, HttpRootPathBuildItem httpRootPathBuildItem, - OutputTargetBuildItem out, SmallRyeOpenApiConfig smallRyeOpenApiConfig, OutputTargetBuildItem outputTargetBuildItem, - List ignoreStaticDocumentBuildItems) throws IOException { - FilteredIndexView index = openApiFilteredIndexViewBuildItem.getIndex(); + List ignoreStaticDocumentBuildItems) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + FilteredIndexView index = openApiFilteredIndexViewBuildItem.getIndex(); Config config = ConfigProvider.getConfig(); - OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); feature.produce(new FeatureBuildItem(Feature.SMALLRYE_OPENAPI)); - List urlIgnorePatterns = new ArrayList<>(); - for (IgnoreStaticDocumentBuildItem isdbi : ignoreStaticDocumentBuildItems) { - urlIgnorePatterns.add(isdbi.getUrlIgnorePattern()); - } - - OpenAPI staticModel = generateStaticModel(smallRyeOpenApiConfig, urlIgnorePatterns, - outputTargetBuildItem.getOutputDirectory(), openApiConfig); + List urlIgnorePatterns = ignoreStaticDocumentBuildItems.stream() + .map(IgnoreStaticDocumentBuildItem::getUrlIgnorePattern) + .toList(); - OpenAPI annotationModel; + SmallRyeOpenAPI.Builder builder = SmallRyeOpenAPI.builder() + .withConfig(config) + .withIndex(index) + .withApplicationClassLoader(loader) + .withScannerClassLoader(loader) + .enableModelReader(true) + .enableStandardStaticFiles(Boolean.FALSE.equals(smallRyeOpenApiConfig.ignoreStaticDocument)) + .withResourceLocator(path -> { + URL locator = loader.getResource(path); + if (locator == null || shouldIgnore(urlIgnorePatterns, locator.toString())) { + return null; + } + return locator; + }) + .withCustomStaticFile(() -> loadAdditionalDocsModel(smallRyeOpenApiConfig, urlIgnorePatterns, + outputTargetBuildItem.getOutputDirectory())) + .enableAnnotationScan(shouldScanAnnotations(capabilities, index)) + .withScannerFilter(getScannerFilter(capabilities, index)) + .withContextRootResolver(getContextRootResolver(config, capabilities, httpRootPathBuildItem)) + .withTypeConverter(getTypeConverter(index, capabilities)) + .enableUnannotatedPathParameters(capabilities.isPresent(Capability.RESTEASY_REACTIVE)) + .enableStandardFilter(false) + .withFilters(openAPIBuildItems.stream().map(AddToOpenAPIDefinitionBuildItem::getOASFilter).toList()); + + getUserDefinedBuildtimeFilters(index).forEach(builder::addFilterName); + + // This should be the final filter to run + builder.addFilter(new DefaultInfoFilter(config)); + + SmallRyeOpenAPI openAPI = builder.build(); + + Stream.of(Map.> entry("JSON", openAPI::toJSON), + Map.> entry("YAML", openAPI::toYAML)) + .forEach(format -> { + String name = OpenApiConstants.BASE_NAME + format.getKey(); + byte[] data = format.getValue().get().getBytes(StandardCharsets.UTF_8); + resourceBuildItemBuildProducer.produce(new GeneratedResourceBuildItem(name, data)); + nativeImageResources.produce(new NativeImageResourceBuildItem(name)); + }); + + SmallRyeOpenAPI finalOpenAPI; + SmallRyeOpenAPI storedOpenAPI; + + Supplier filterOnlyBuilder = () -> { + var runtimeFilterBuilder = SmallRyeOpenAPI.builder() + .enableModelReader(false) + .enableStandardStaticFiles(false) + .enableAnnotationScan(false) + .enableStandardFilter(false) + .withInitialModel(openAPI.model()); + Optional.ofNullable(getAutoServerFilter(smallRyeOpenApiConfig, true, "Auto generated value")) + .ifPresent(runtimeFilterBuilder::addFilter); + return runtimeFilterBuilder; + }; - if (shouldScanAnnotations(capabilities, index)) { - annotationModel = generateAnnotationModel(index, capabilities, httpRootPathBuildItem, config, openApiConfig); - } else { - annotationModel = new OpenAPIImpl(); + try { + builder = filterOnlyBuilder.get(); + getUserDefinedRuntimeFilters(config, index).forEach(builder::addFilterName); + storedOpenAPI = builder.build(); + } catch (Exception e) { + // Try again without the user-defined runtime filters + storedOpenAPI = filterOnlyBuilder.get().build(); } - OpenApiDocument finalDocument = loadDocument(staticModel, annotationModel, openAPIBuildItems, index); - for (Format format : Format.values()) { - String name = OpenApiConstants.BASE_NAME + format; - byte[] schemaDocument = OpenApiSerializer.serialize(finalDocument.get(), format).getBytes(StandardCharsets.UTF_8); - resourceBuildItemBuildProducer.produce(new GeneratedResourceBuildItem(name, schemaDocument)); - nativeImageResources.produce(new NativeImageResourceBuildItem(name)); - } + finalOpenAPI = storedOpenAPI; + + smallRyeOpenApiConfig.storeSchemaDirectory.ifPresent(storageDir -> { + try { + storeGeneratedSchema(storageDir, outputTargetBuildItem, finalOpenAPI.toJSON(), "json"); + storeGeneratedSchema(storageDir, outputTargetBuildItem, finalOpenAPI.toYAML(), "yaml"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); - OpenApiDocument finalStoredOpenApiDocument = storeDocument(out, smallRyeOpenApiConfig, index, finalDocument.get()); - openApiDocumentProducer.produce(new OpenApiDocumentBuildItem(finalStoredOpenApiDocument)); + OpenApiDocument output = OpenApiDocument.newInstance(); + output.set(finalOpenAPI.model()); + openApiDocumentProducer.produce(new OpenApiDocumentBuildItem(output)); } @BuildStep @@ -929,10 +954,8 @@ private void produceReflectiveHierarchy(BuildProducer ignorePatterns, Path target, - OpenApiConfig openApiConfig) - throws IOException { - - if (smallRyeOpenApiConfig.ignoreStaticDocument) { + private InputStream loadAdditionalDocsModel(SmallRyeOpenApiConfig openApiConfig, List ignorePatterns, + Path target) { + if (openApiConfig.ignoreStaticDocument) { return null; - } else { - List results = findStaticModels(smallRyeOpenApiConfig, ignorePatterns, target); - if (!results.isEmpty()) { - OpenAPI mergedStaticModel = new OpenAPIImpl(); - for (Result result : results) { - try (InputStream is = result.inputStream; - OpenApiStaticFile staticFile = new OpenApiStaticFile(is, result.format)) { - OpenAPI staticFileModel = io.smallrye.openapi.runtime.OpenApiProcessor - .modelFromStaticFile(openApiConfig, staticFile); - mergedStaticModel = MergeUtil.mergeObjects(mergedStaticModel, staticFileModel); - } - } - return mergedStaticModel; - } - return null; - } - } - - private OpenAPI generateAnnotationModel(IndexView indexView, Capabilities capabilities, - HttpRootPathBuildItem httpRootPathBuildItem, - Config config, OpenApiConfig openApiConfig) { - - List extensions = new ArrayList<>(); - - // Add the RESTEasy extension if the capability is present - String rootPath = httpRootPathBuildItem.getRootPath(); - String appPath = ""; - - if (capabilities.isPresent(Capability.RESTEASY)) { - extensions.add(new RESTEasyExtension(indexView)); - appPath = config.getOptionalValue("quarkus.resteasy.path", String.class).orElse(""); - } else if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { - extensions.add(new RESTEasyExtension(indexView)); - openApiConfig.doAllowNakedPathParameter(); - appPath = config.getOptionalValue("quarkus.rest.path", String.class).orElse(""); } - extensions.add(new CustomPathExtension(rootPath, appPath)); + SmallRyeOpenAPI.Builder staticBuilder = SmallRyeOpenAPI.builder() + .withConfig(ConfigProvider.getConfig()) + .enableModelReader(false) + .enableStandardStaticFiles(false) + .enableAnnotationScan(false) + .enableStandardFilter(false); - OpenApiAnnotationScanner openApiAnnotationScanner = new OpenApiAnnotationScanner(openApiConfig, indexView, extensions); - return openApiAnnotationScanner.scan(getScanners(capabilities, indexView)); + return openApiConfig.additionalDocsDirectory + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(path -> getResourceFiles(path, target)) + .flatMap(Collection::stream) + .filter(path -> path.endsWith(".json") || path.endsWith(".yaml") || path.endsWith(".yml")) + .flatMap(path -> loadResources(path, ignorePatterns)) + .map(stream -> staticBuilder.withCustomStaticFile(() -> stream).build().model()) + .reduce(MergeUtil::merge) + .map(mergedModel -> staticBuilder + .withInitialModel(mergedModel) + .withCustomStaticFile(() -> null) + .build() + .toJSON()) + .map(jsonModel -> new ByteArrayInputStream(jsonModel.getBytes(StandardCharsets.UTF_8))) + .orElse(null); } - private String[] getScanners(Capabilities capabilities, IndexView index) { + private Predicate getScannerFilter(Capabilities capabilities, IndexView index) { List scanners = new ArrayList<>(); if (capabilities.isPresent(Capability.RESTEASY) || capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { scanners.add(JAX_RS); @@ -1038,78 +1043,59 @@ private String[] getScanners(Capabilities capabilities, IndexView index) { if (isUsingVertxRoute(index)) { scanners.add(VERT_X); } - return scanners.toArray(new String[] {}); + return scanners::contains; } - private List findStaticModels(SmallRyeOpenApiConfig openApiConfig, List ignorePatterns, Path target) { - List results = new ArrayList<>(); - - // First check for the file in both META-INF and WEB-INF/classes/META-INF - addStaticModelIfExist(results, ignorePatterns, Format.YAML, META_INF_OPENAPI_YAML); - addStaticModelIfExist(results, ignorePatterns, Format.YAML, WEB_INF_CLASSES_META_INF_OPENAPI_YAML); - addStaticModelIfExist(results, ignorePatterns, Format.YAML, META_INF_OPENAPI_YML); - addStaticModelIfExist(results, ignorePatterns, Format.YAML, WEB_INF_CLASSES_META_INF_OPENAPI_YML); - addStaticModelIfExist(results, ignorePatterns, Format.JSON, META_INF_OPENAPI_JSON); - addStaticModelIfExist(results, ignorePatterns, Format.JSON, WEB_INF_CLASSES_META_INF_OPENAPI_JSON); + private Function, String> getContextRootResolver(Config config, Capabilities capabilities, + HttpRootPathBuildItem httpRootPathBuildItem) { + String rootPath = httpRootPathBuildItem.getRootPath(); + String appPath = ""; - // Add any additional directories if configured - if (openApiConfig.additionalDocsDirectory.isPresent()) { - List additionalStaticDocuments = openApiConfig.additionalDocsDirectory.get(); - for (Path path : additionalStaticDocuments) { - // Scan all yaml and json files - try { - List filesInDir = getResourceFiles(path, target); - for (String possibleModelFile : filesInDir) { - addStaticModelIfExist(results, ignorePatterns, possibleModelFile); - } - } catch (IOException ioe) { - throw new UncheckedIOException("An error occurred while processing " + path, ioe); - } - } + if (capabilities.isPresent(Capability.RESTEASY)) { + appPath = config.getOptionalValue("quarkus.resteasy.path", String.class).orElse(""); + } else if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { + appPath = config.getOptionalValue("quarkus.rest.path", String.class).orElse(""); } - return results; + var resolver = new CustomPathExtension(rootPath, appPath); + return resolver::resolveContextRoot; } - private void addStaticModelIfExist(List results, List ignorePatterns, String path) { - if (path.endsWith(".json")) { - // Scan a specific json file - addStaticModelIfExist(results, ignorePatterns, Format.JSON, path); - } else if (path.endsWith(".yaml") || path.endsWith(".yml")) { - // Scan a specific yaml file - addStaticModelIfExist(results, ignorePatterns, Format.YAML, path); + private UnaryOperator getTypeConverter(IndexView indexView, Capabilities capabilities) { + if (capabilities.isPresent(Capability.RESTEASY) || capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { + return new RESTEasyExtension(indexView)::resolveAsyncType; + } else { + return UnaryOperator.identity(); } } - private void addStaticModelIfExist(List results, List ignorePatterns, Format format, String path) { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); + private Stream loadResources(String path, List ignorePatterns) { + Spliterator resources; try { - Enumeration urls = cl.getResources(path); - while (urls.hasMoreElements()) { - URL url = urls.nextElement(); - // Check if we should ignore - String urlAsString = url.toString(); - if (!shouldIgnore(ignorePatterns, urlAsString)) { - // Add as static model - URLConnection con = url.openConnection(); - con.setUseCaches(false); - try (InputStream inputStream = con.getInputStream()) { - if (inputStream != null) { - byte[] contents = IoUtil.readBytes(inputStream); - - results.add(new Result(format, new ByteArrayInputStream(contents))); - } - } catch (IOException ex) { - throw new UncheckedIOException("An error occurred while processing " + urlAsString + " for " + path, - ex); - } - } - } - + var resourceEnum = Thread.currentThread().getContextClassLoader().getResources(path).asIterator(); + resources = Spliterators.spliteratorUnknownSize(resourceEnum, Spliterator.IMMUTABLE); } catch (IOException ex) { - throw new UncheckedIOException(ex); + throw new UncheckedIOException("Exception processing resources for path " + path, ex); } + + return StreamSupport.stream(resources, false) + .filter(url -> !shouldIgnore(ignorePatterns, url.toString())) + .map(url -> loadResource(path, url)) + .filter(Objects::nonNull); + } + + private InputStream loadResource(String path, URL url) { + try (InputStream inputStream = url.openStream()) { + if (inputStream != null) { + byte[] contents = IoUtil.readBytes(inputStream); + return new ByteArrayInputStream(contents); + } + } catch (IOException e) { + throw new UncheckedIOException("An error occurred while processing %s for %s".formatted(url, path), e); + } + + return null; } private boolean shouldIgnore(List ignorePatterns, String url) { @@ -1122,7 +1108,7 @@ private boolean shouldIgnore(List ignorePatterns, String url) { return false; } - private List getResourceFiles(Path resourcePath, Path target) throws IOException { + private List getResourceFiles(Path resourcePath, Path target) { final String resourceName = ClassPathUtils.toResourceName(resourcePath); List filenames = new ArrayList<>(); // Here we are resolving the resource dir relative to the classes dir and if it does not exist, we fall back to locating the resource dir on the classpath. @@ -1132,6 +1118,8 @@ private List getResourceFiles(Path resourcePath, Path target) throws IOE if (targetResourceDir != null && Files.exists(targetResourceDir)) { try (Stream paths = Files.list(targetResourceDir)) { return paths.map(t -> resourceName + "/" + t.getFileName().toString()).toList(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred while processing " + resourcePath, e); } } else { ClassLoader cl = Thread.currentThread().getContextClassLoader(); @@ -1144,128 +1132,10 @@ private List getResourceFiles(Path resourcePath, Path target) throws IOE } } } + } catch (IOException e) { + throw new UncheckedIOException("An error occurred while processing " + resourcePath, e); } } return filenames; } - - static class Result { - final Format format; - final InputStream inputStream; - - Result(Format format, InputStream inputStream) { - this.format = format; - this.inputStream = inputStream; - } - } - - private OpenApiDocument loadDocument(OpenAPI staticModel, OpenAPI annotationModel, - List openAPIBuildItems, IndexView index) { - OpenApiDocument document = prepareOpenApiDocument(staticModel, annotationModel, openAPIBuildItems, index, true); - - Config c = ConfigProvider.getConfig(); - String title = c.getOptionalValue("quarkus.application.name", String.class).orElse("Generated"); - String version = c.getOptionalValue("quarkus.application.version", String.class).orElse("1.0"); - - document.archiveName(title); - document.version(version); - - document.initialize(); - return document; - } - - private OpenApiDocument storeDocument(OutputTargetBuildItem out, - SmallRyeOpenApiConfig smallRyeOpenApiConfig, - IndexView index, - OpenAPI loadedModel) throws IOException { - return storeDocument(out, smallRyeOpenApiConfig, index, loadedModel, true); - } - - private OpenApiDocument storeDocument(OutputTargetBuildItem out, - SmallRyeOpenApiConfig smallRyeOpenApiConfig, - IndexView index, - OpenAPI loadedModel, - boolean includeRuntimeFilters) throws IOException { - - Config config = ConfigProvider.getConfig(); - OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); - - OpenApiDocument document = prepareOpenApiDocument(loadedModel, null, Collections.emptyList(), index, false); - - if (includeRuntimeFilters) { - List userDefinedRuntimeFilters = getUserDefinedRuntimeFilters(openApiConfig, index); - for (String s : userDefinedRuntimeFilters) { - document.filter(filter(s, index)); // This usually happens at runtime, so when storing we want to filter here too. - } - } - - // By default, also add the auto generated server - OASFilter autoServerFilter = getAutoServerFilter(smallRyeOpenApiConfig, true, "Auto generated value"); - if (autoServerFilter != null) { - document.filter(autoServerFilter); - } - - try { - document.initialize(); - } catch (RuntimeException re) { - if (includeRuntimeFilters) { - // This is a Runtime filter, so it might not work at build time. In that case we ignore the filter. - return storeDocument(out, smallRyeOpenApiConfig, index, loadedModel, false); - } else { - throw re; - } - } - // Store the document if needed - boolean shouldStore = smallRyeOpenApiConfig.storeSchemaDirectory.isPresent(); - if (shouldStore) { - for (Format format : Format.values()) { - byte[] schemaDocument = OpenApiSerializer.serialize(document.get(), format).getBytes(StandardCharsets.UTF_8); - storeGeneratedSchema(smallRyeOpenApiConfig, out, schemaDocument, format); - } - } - - return document; - } - - private OpenApiDocument prepareOpenApiDocument(OpenAPI staticModel, - OpenAPI annotationModel, - List openAPIBuildItems, - IndexView index, - boolean executeBuildFilters) { - Config config = ConfigProvider.getConfig(); - OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); - - OpenAPI readerModel = OpenApiProcessor.modelFromReader(openApiConfig, - Thread.currentThread().getContextClassLoader(), index); - - OpenApiDocument document = createDocument(openApiConfig); - if (annotationModel != null) { - document.modelFromAnnotations(annotationModel); - } - document.modelFromReader(readerModel); - document.modelFromStaticFile(staticModel); - for (AddToOpenAPIDefinitionBuildItem openAPIBuildItem : openAPIBuildItems) { - OASFilter otherExtensionFilter = openAPIBuildItem.getOASFilter(); - document.filter(otherExtensionFilter); - } - // Add user defined Build time filters if necessary - if (executeBuildFilters) { - List userDefinedFilters = getUserDefinedBuildtimeFilters(index); - for (String filter : userDefinedFilters) { - document.filter(filter(filter, index)); - } - } - return document; - } - - private OpenApiDocument createDocument(OpenApiConfig openApiConfig) { - OpenApiDocument document = OpenApiDocument.INSTANCE; - document.reset(); - document.config(openApiConfig); - return document; - } - - private OASFilter filter(String className, IndexView index) { - return OpenApiProcessor.getFilter(className, Thread.currentThread().getContextClassLoader(), index); - } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/DefaultInfoFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/DefaultInfoFilter.java new file mode 100644 index 0000000000000..1dbbbed3fc136 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/DefaultInfoFilter.java @@ -0,0 +1,35 @@ +package io.quarkus.smallrye.openapi.deployment.filter; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.info.Info; + +public class DefaultInfoFilter implements OASFilter { + + final Config config; + + public DefaultInfoFilter(Config config) { + this.config = config; + } + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + Info info = openAPI.getInfo(); + + if (info == null) { + info = OASFactory.createInfo(); + openAPI.setInfo(info); + } + + if (info.getTitle() == null) { + String title = config.getOptionalValue("quarkus.application.name", String.class).orElse("Generated"); + info.setTitle(title + " API"); + } + if (info.getVersion() == null) { + String version = config.getOptionalValue("quarkus.application.version", String.class).orElse("1.0"); + info.setVersion((version == null ? "1.0" : version)); + } + } +} diff --git a/extensions/smallrye-openapi/deployment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/extensions/smallrye-openapi/deployment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource new file mode 100644 index 0000000000000..8195a1e2b5c7d --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource @@ -0,0 +1 @@ +io.quarkus.smallrye.openapi.deployment.MediaTypeConfigSource \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtensionTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtensionTest.java index ce08b78906828..3c60b349a6c0a 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtensionTest.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/deployment/CustomPathExtensionTest.java @@ -1,32 +1,24 @@ package io.quarkus.smallrye.openapi.deployment; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + import java.io.IOException; import java.util.Collections; import java.util.List; import org.jboss.jandex.Index; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.mockito.Mockito; - -import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner; class CustomPathExtensionTest { - AnnotationScanner scanner; - - @BeforeEach - void setup() { - scanner = Mockito.mock(AnnotationScanner.class); - } - @Test void testContextPathNotInvokedForEmptyPaths() { CustomPathExtension ext = new CustomPathExtension("/", ""); - ext.processScannerApplications(scanner, Collections.emptyList()); - Mockito.verify(scanner, Mockito.never()).setContextRoot(Mockito.anyString()); + String contextRoot = ext.resolveContextRoot(Collections.emptyList()); + assertNull(contextRoot); } @ParameterizedTest @@ -38,8 +30,8 @@ void testContextPathNotInvokedForEmptyPaths() { }) void testContextPathGenerationWithoutApplicationPathAnnotation(String rootPath, String appPath, String expected) { CustomPathExtension ext = new CustomPathExtension(rootPath, appPath); - ext.processScannerApplications(scanner, Collections.emptyList()); - Mockito.verify(scanner).setContextRoot(expected); + String contextRoot = ext.resolveContextRoot(Collections.emptyList()); + assertEquals(expected, contextRoot); } @ParameterizedTest @@ -56,8 +48,8 @@ class TestApp extends jakarta.ws.rs.core.Application { } CustomPathExtension ext = new CustomPathExtension(rootPath, appPath); - ext.processScannerApplications(scanner, List.of(Index.of(TestApp.class).getClassByName(TestApp.class))); - Mockito.verify(scanner, Mockito.times(times)).setContextRoot(times > 0 ? expected : Mockito.anyString()); + String contextRoot = ext.resolveContextRoot(List.of(Index.of(TestApp.class).getClassByName(TestApp.class))); + assertEquals(expected, contextRoot); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithConfigTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithConfigTestCase.java index 1377a31316632..f37cb6cd6c58c 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithConfigTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithConfigTestCase.java @@ -35,6 +35,6 @@ public void testOpenAPI() { .body("paths", Matchers.hasKey("/openapi")) .body("paths", Matchers.not(Matchers.hasKey("/resource"))); - System.clearProperty(io.smallrye.openapi.api.constants.OpenApiConstants.INFO_TITLE); + System.clearProperty(io.smallrye.openapi.api.SmallRyeOASConfig.INFO_TITLE); } } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiConfigMapping.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiConfigMapping.java index 2a9e85afd638a..9d1fdb11ad2ce 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiConfigMapping.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiConfigMapping.java @@ -5,17 +5,20 @@ import java.util.Map; import org.eclipse.microprofile.config.spi.Converter; +import org.eclipse.microprofile.openapi.OASConfig; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; import io.smallrye.config.Converters; import io.smallrye.config.RelocateConfigSourceInterceptor; import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy; +import io.smallrye.openapi.api.SmallRyeOASConfig; /** * Maps config from MicroProfile and SmallRye to Quarkus */ public class OpenApiConfigMapping extends RelocateConfigSourceInterceptor { + private static final long serialVersionUID = 1L; private static final Map RELOCATIONS = relocations(); private static final Converter OPERATION_ID_STRATEGY_CONVERTER = Converters .getImplicitConverter(OperationIdStrategy.class); @@ -27,31 +30,30 @@ public OpenApiConfigMapping() { @Override public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { ConfigValue configValue = super.getValue(context, name); + // Special case for enum. The converter run after the interceptors, so we have to do this here. - if (name.equals(io.smallrye.openapi.api.constants.OpenApiConstants.OPERATION_ID_STRAGEGY)) { - if (configValue != null) { - String correctValue = OPERATION_ID_STRATEGY_CONVERTER.convert(configValue.getValue()).toString(); - configValue = configValue.withValue(correctValue); - } + if (configValue != null && name.equals(SmallRyeOASConfig.OPERATION_ID_STRAGEGY)) { + String correctValue = OPERATION_ID_STRATEGY_CONVERTER.convert(configValue.getValue()).toString(); + configValue = configValue.withValue(correctValue); } + return configValue; } private static Map relocations() { Map relocations = new HashMap<>(); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.VERSION, QUARKUS_OPEN_API_VERSION); - mapKey(relocations, org.eclipse.microprofile.openapi.OASConfig.SERVERS, QUARKUS_SERVERS); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_TITLE, QUARKUS_INFO_TITLE); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_VERSION, QUARKUS_INFO_VERSION); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_DESCRIPTION, QUARKUS_INFO_DESCRIPTION); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_TERMS, QUARKUS_INFO_TERMS); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_CONTACT_EMAIL, QUARKUS_INFO_CONTACT_EMAIL); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_CONTACT_NAME, QUARKUS_INFO_CONTACT_NAME); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_CONTACT_URL, QUARKUS_INFO_CONTACT_URL); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_LICENSE_NAME, QUARKUS_INFO_LICENSE_NAME); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.INFO_LICENSE_URL, QUARKUS_INFO_LICENSE_URL); - mapKey(relocations, io.smallrye.openapi.api.constants.OpenApiConstants.OPERATION_ID_STRAGEGY, - QUARKUS_OPERATION_ID_STRATEGY); + mapKey(relocations, SmallRyeOASConfig.VERSION, QUARKUS_OPEN_API_VERSION); + mapKey(relocations, OASConfig.SERVERS, QUARKUS_SERVERS); + mapKey(relocations, SmallRyeOASConfig.INFO_TITLE, QUARKUS_INFO_TITLE); + mapKey(relocations, SmallRyeOASConfig.INFO_VERSION, QUARKUS_INFO_VERSION); + mapKey(relocations, SmallRyeOASConfig.INFO_DESCRIPTION, QUARKUS_INFO_DESCRIPTION); + mapKey(relocations, SmallRyeOASConfig.INFO_TERMS, QUARKUS_INFO_TERMS); + mapKey(relocations, SmallRyeOASConfig.INFO_CONTACT_EMAIL, QUARKUS_INFO_CONTACT_EMAIL); + mapKey(relocations, SmallRyeOASConfig.INFO_CONTACT_NAME, QUARKUS_INFO_CONTACT_NAME); + mapKey(relocations, SmallRyeOASConfig.INFO_CONTACT_URL, QUARKUS_INFO_CONTACT_URL); + mapKey(relocations, SmallRyeOASConfig.INFO_LICENSE_NAME, QUARKUS_INFO_LICENSE_NAME); + mapKey(relocations, SmallRyeOASConfig.INFO_LICENSE_URL, QUARKUS_INFO_LICENSE_URL); + mapKey(relocations, SmallRyeOASConfig.OPERATION_ID_STRAGEGY, QUARKUS_OPERATION_ID_STRATEGY); return Collections.unmodifiableMap(relocations); } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java index 99b85cdd96750..8d9676a3c0171 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java @@ -2,27 +2,21 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.openapi.OASConfig; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.jboss.jandex.IndexView; -import org.jboss.jandex.Indexer; import io.quarkus.smallrye.openapi.runtime.filter.DisabledRestEndpointsFilter; -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.io.OpenApiSerializer; +import io.smallrye.openapi.api.SmallRyeOpenAPI; /** * Loads the document and make it available @@ -30,17 +24,31 @@ @ApplicationScoped public class OpenApiDocumentService implements OpenApiDocumentHolder { - private static final IndexView EMPTY_INDEX = new Indexer().complete(); private final OpenApiDocumentHolder documentHolder; @Inject public OpenApiDocumentService(OASFilter autoSecurityFilter, OpenApiRecorder.UserDefinedRuntimeFilters userDefinedRuntimeFilters, Config config) { - if (config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class).orElse(Boolean.FALSE)) { - this.documentHolder = new DynamicDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters()); - } else { - this.documentHolder = new StaticDocument(config, autoSecurityFilter, userDefinedRuntimeFilters.filters()); + ClassLoader loader = Optional.ofNullable(OpenApiConstants.classLoader) + .orElseGet(Thread.currentThread()::getContextClassLoader); + + try (InputStream source = loader.getResourceAsStream(OpenApiConstants.BASE_NAME + "JSON")) { + if (source != null) { + var userFilters = userDefinedRuntimeFilters.filters(); + boolean dynamic = config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class) + .orElse(Boolean.FALSE); + + if (dynamic) { + this.documentHolder = new DynamicDocument(source, config, autoSecurityFilter, userFilters); + } else { + this.documentHolder = new StaticDocument(source, config, autoSecurityFilter, userFilters); + } + } else { + this.documentHolder = new EmptyDocument(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); } } @@ -52,6 +60,18 @@ public byte[] getYamlDocument() { return this.documentHolder.getYamlDocument(); } + static class EmptyDocument implements OpenApiDocumentHolder { + static final byte[] EMPTY = new byte[0]; + + public byte[] getJsonDocument() { + return EMPTY; + } + + public byte[] getYamlDocument() { + return EMPTY; + } + } + /** * Generate the document once on creation. */ @@ -60,39 +80,22 @@ static class StaticDocument implements OpenApiDocumentHolder { private byte[] jsonDocument; private byte[] yamlDocument; - StaticDocument(Config config, OASFilter autoFilter, List userFilters) { - ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() - : OpenApiConstants.classLoader; - try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { - if (is != null) { - try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) { - - OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); - - OpenApiDocument document = OpenApiDocument.INSTANCE; - document.reset(); - document.config(openApiConfig); - document.modelFromStaticFile(OpenApiProcessor.modelFromStaticFile(openApiConfig, staticFile)); - if (autoFilter != null) { - document.filter(autoFilter); - } - document.filter(new DisabledRestEndpointsFilter()); - for (String userFilter : userFilters) { - document.filter(OpenApiProcessor.getFilter(userFilter, cl, EMPTY_INDEX)); - } - document.initialize(); - - this.jsonDocument = OpenApiSerializer.serialize(document.get(), Format.JSON) - .getBytes(StandardCharsets.UTF_8); - this.yamlDocument = OpenApiSerializer.serialize(document.get(), Format.YAML) - .getBytes(StandardCharsets.UTF_8); - document.reset(); - document = null; - } - } - } catch (IOException ex) { - throw new RuntimeException("Could not find [" + OpenApiConstants.BASE_NAME + Format.JSON + "]"); - } + StaticDocument(InputStream source, Config config, OASFilter autoFilter, List userFilters) { + SmallRyeOpenAPI.Builder builder = SmallRyeOpenAPI.builder() + .withConfig(config) + .enableModelReader(false) + .enableStandardStaticFiles(false) + .enableAnnotationScan(false) + .enableStandardFilter(false) + .withCustomStaticFile(() -> source); + + Optional.ofNullable(autoFilter).ifPresent(builder::addFilter); + builder.addFilter(new DisabledRestEndpointsFilter()); + userFilters.forEach(builder::addFilterName); + + SmallRyeOpenAPI openAPI = builder.build(); + jsonDocument = openAPI.toJSON().getBytes(StandardCharsets.UTF_8); + yamlDocument = openAPI.toYAML().getBytes(StandardCharsets.UTF_8); } public byte[] getJsonDocument() { @@ -109,78 +112,35 @@ public byte[] getYamlDocument() { */ static class DynamicDocument implements OpenApiDocumentHolder { + private SmallRyeOpenAPI.Builder builder; private OpenAPI generatedOnBuild; - private OpenApiConfig openApiConfig; - private List userFilters = new ArrayList<>(); - private OASFilter autoFilter; - private DisabledRestEndpointsFilter disabledEndpointsFilter; - - DynamicDocument(Config config, OASFilter autoFilter, List annotatedUserFilters) { - ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() - : OpenApiConstants.classLoader; - try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { - if (is != null) { - try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) { - this.openApiConfig = new OpenApiConfigImpl(config); - OASFilter microProfileDefinedFilter = OpenApiProcessor.getFilter(openApiConfig, cl, EMPTY_INDEX); - if (microProfileDefinedFilter != null) { - userFilters.add(microProfileDefinedFilter); - } - for (String annotatedUserFilter : annotatedUserFilters) { - OASFilter annotatedUserDefinedFilter = OpenApiProcessor.getFilter(annotatedUserFilter, cl, - EMPTY_INDEX); - userFilters.add(annotatedUserDefinedFilter); - } - this.autoFilter = autoFilter; - this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(this.openApiConfig, staticFile); - this.disabledEndpointsFilter = new DisabledRestEndpointsFilter(); - } - } - } catch (IOException ex) { - throw new RuntimeException("Could not find [" + OpenApiConstants.BASE_NAME + Format.JSON + "]"); - } + + DynamicDocument(InputStream source, Config config, OASFilter autoFilter, List annotatedUserFilters) { + builder = SmallRyeOpenAPI.builder() + .withConfig(config) + .enableModelReader(false) + .enableStandardStaticFiles(false) + .enableAnnotationScan(false) + .enableStandardFilter(false) + .withCustomStaticFile(() -> source); + + generatedOnBuild = builder.build().model(); + + builder.withCustomStaticFile(() -> null); + builder.withInitialModel(generatedOnBuild); + + Optional.ofNullable(autoFilter).ifPresent(builder::addFilter); + builder.addFilter(new DisabledRestEndpointsFilter()); + config.getOptionalValue(OASConfig.FILTER, String.class).ifPresent(builder::addFilterName); + annotatedUserFilters.forEach(builder::addFilterName); } public byte[] getJsonDocument() { - try { - OpenApiDocument document = getOpenApiDocument(); - byte[] jsonDocument = OpenApiSerializer.serialize(document.get(), Format.JSON) - .getBytes(StandardCharsets.UTF_8); - document.reset(); - document = null; - return jsonDocument; - } catch (IOException ex) { - throw new RuntimeException(ex); - } + return builder.build().toJSON().getBytes(StandardCharsets.UTF_8); } public byte[] getYamlDocument() { - try { - OpenApiDocument document = getOpenApiDocument(); - byte[] yamlDocument = OpenApiSerializer.serialize(document.get(), Format.YAML) - .getBytes(StandardCharsets.UTF_8); - document.reset(); - document = null; - return yamlDocument; - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private OpenApiDocument getOpenApiDocument() { - OpenApiDocument document = OpenApiDocument.INSTANCE; - document.reset(); - document.config(this.openApiConfig); - document.modelFromStaticFile(this.generatedOnBuild); - if (this.autoFilter != null) { - document.filter(this.autoFilter); - } - document.filter(this.disabledEndpointsFilter); - for (OASFilter userFilter : userFilters) { - document.filter(userFilter); - } - document.initialize(); - return document; + return builder.build().toYAML().getBytes(StandardCharsets.UTF_8); } } }