PageRenderTime 55ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/microprofile/openapi-smallrye/src/main/java/org/wildfly/extension/microprofile/openapi/deployment/OpenAPIModelServiceConfigurator.java

https://github.com/ochaloup/jboss-as
Java | 280 lines | 216 code | 27 blank | 37 comment | 30 complexity | 8a9fa9f5111d8d503ebc94b3071e1384 MD5 | raw file
  1. /*
  2. * JBoss, Home of Professional Open Source.
  3. * Copyright 2019, Red Hat, Inc., and individual contributors
  4. * as indicated by the @author tags. See the copyright.txt file in the
  5. * distribution for a full listing of individual contributors.
  6. *
  7. * This is free software; you can redistribute it and/or modify it
  8. * under the terms of the GNU Lesser General Public License as
  9. * published by the Free Software Foundation; either version 2.1 of
  10. * the License, or (at your option) any later version.
  11. *
  12. * This software is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public
  18. * License along with this software; if not, write to the Free
  19. * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  20. * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  21. */
  22. package org.wildfly.extension.microprofile.openapi.deployment;
  23. import static org.wildfly.extension.microprofile.openapi.logging.MicroProfileOpenAPILogger.LOGGER;
  24. import java.io.IOException;
  25. import java.net.InetAddress;
  26. import java.net.MalformedURLException;
  27. import java.net.URL;
  28. import java.util.AbstractMap;
  29. import java.util.ArrayList;
  30. import java.util.Arrays;
  31. import java.util.Collection;
  32. import java.util.Collections;
  33. import java.util.EnumMap;
  34. import java.util.EnumSet;
  35. import java.util.List;
  36. import java.util.Map;
  37. import java.util.Set;
  38. import java.util.TreeSet;
  39. import java.util.function.Consumer;
  40. import java.util.function.Function;
  41. import java.util.function.Supplier;
  42. import java.util.stream.Collectors;
  43. import org.eclipse.microprofile.config.Config;
  44. import org.eclipse.microprofile.openapi.OASFactory;
  45. import org.eclipse.microprofile.openapi.models.OpenAPI;
  46. import org.eclipse.microprofile.openapi.models.info.Info;
  47. import org.eclipse.microprofile.openapi.models.servers.Server;
  48. import org.eclipse.microprofile.openapi.spi.OASFactoryResolver;
  49. import org.jboss.as.network.ClientMapping;
  50. import org.jboss.as.network.SocketBinding;
  51. import org.jboss.as.server.deployment.Attachments;
  52. import org.jboss.as.server.deployment.DeploymentUnit;
  53. import org.jboss.as.web.common.WarMetaData;
  54. import org.jboss.jandex.CompositeIndex;
  55. import org.jboss.jandex.Index;
  56. import org.jboss.jandex.IndexView;
  57. import org.jboss.metadata.javaee.spec.DescriptionGroupMetaData;
  58. import org.jboss.metadata.web.jboss.JBossWebMetaData;
  59. import org.jboss.modules.Module;
  60. import org.jboss.msc.Service;
  61. import org.jboss.msc.service.ServiceBuilder;
  62. import org.jboss.msc.service.ServiceName;
  63. import org.jboss.msc.service.ServiceTarget;
  64. import org.jboss.vfs.VirtualFile;
  65. import org.wildfly.clustering.service.CompositeDependency;
  66. import org.wildfly.clustering.service.FunctionalService;
  67. import org.wildfly.clustering.service.ServiceConfigurator;
  68. import org.wildfly.clustering.service.ServiceSupplierDependency;
  69. import org.wildfly.clustering.service.SimpleServiceNameProvider;
  70. import org.wildfly.clustering.service.SupplierDependency;
  71. import org.wildfly.extension.microprofile.openapi.logging.MicroProfileOpenAPILogger;
  72. import org.wildfly.extension.undertow.Capabilities;
  73. import org.wildfly.extension.undertow.Host;
  74. import org.wildfly.extension.undertow.UndertowListener;
  75. import org.wildfly.extension.undertow.UndertowService;
  76. import org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService;
  77. import io.smallrye.openapi.api.OpenApiConfig;
  78. import io.smallrye.openapi.api.OpenApiConfigImpl;
  79. import io.smallrye.openapi.runtime.OpenApiProcessor;
  80. import io.smallrye.openapi.runtime.OpenApiStaticFile;
  81. import io.smallrye.openapi.runtime.io.Format;
  82. import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
  83. import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner;
  84. import io.smallrye.openapi.spi.OASFactoryResolverImpl;
  85. import io.undertow.servlet.api.DeploymentInfo;
  86. /**
  87. * Configures a service that provides an OpenAPI model for a deployment.
  88. * @author Paul Ferraro
  89. */
  90. public class OpenAPIModelServiceConfigurator extends SimpleServiceNameProvider implements OpenAPIServiceNameProvider, ServiceConfigurator, Supplier<OpenAPI> {
  91. private static final String PATH = "mp.openapi.extensions.path";
  92. private static final String DEFAULT_PATH = "/openapi";
  93. private static final String RELATIVE_SERVER_URLS = "mp.openapi.extensions.servers.relative";
  94. private static final String DEFAULT_TITLE = "Generated API";
  95. private static final Set<String> REQUISITE_LISTENERS = Collections.singleton("http");
  96. private static final Map<Format, List<String>> STATIC_FILES = new EnumMap<>(Format.class);
  97. static {
  98. // Order resource names by search order
  99. STATIC_FILES.put(Format.YAML, Arrays.asList(
  100. "/META-INF/openapi.yaml",
  101. "/WEB-INF/classes/META-INF/openapi.yaml",
  102. "/META-INF/openapi.yml",
  103. "/WEB-INF/classes/META-INF/openapi.yml"));
  104. STATIC_FILES.put(Format.JSON, Arrays.asList(
  105. "/META-INF/openapi.json",
  106. "/WEB-INF/classes/META-INF/openapi.json"));
  107. // Set the static OASFactoryResolver eagerly avoiding the need perform TCCL service loading later
  108. OASFactoryResolver.setInstance(new OASFactoryResolverImpl());
  109. }
  110. private final Config config;
  111. private final String deploymentName;
  112. private final VirtualFile root;
  113. private final CompositeIndex index;
  114. private final Module module;
  115. private final JBossWebMetaData metaData;
  116. private final SupplierDependency<Host> host;
  117. private final SupplierDependency<DeploymentInfo> info;
  118. public OpenAPIModelServiceConfigurator(DeploymentUnit unit, String serverName, String hostName, Config config) {
  119. super(unit.getAttachment(Attachments.CAPABILITY_SERVICE_SUPPORT).getCapabilityServiceName(Capabilities.CAPABILITY_HOST, serverName, hostName).append(config.getOptionalValue(PATH, String.class).orElse(DEFAULT_PATH)));
  120. this.deploymentName = unit.getName();
  121. this.root = unit.getAttachment(Attachments.DEPLOYMENT_ROOT).getRoot();
  122. // Convert org.jboss.as.server.deployment.annotation.CompositeIndex to org.jboss.jandex.CompositeIndex
  123. Collection<Index> indexes = new ArrayList<>(unit.getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes());
  124. if (unit.getParent() != null) {
  125. // load all composite indexes of the parent deployment unit
  126. indexes.addAll(unit.getParent().getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes());
  127. // load all composite indexes of the parent's accessible sub deployments
  128. unit.getParent()
  129. .getAttachment(Attachments.ACCESSIBLE_SUB_DEPLOYMENTS)
  130. .forEach(subdeployment -> indexes.addAll(
  131. subdeployment.getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes()));
  132. }
  133. this.index = CompositeIndex.create(indexes.stream().map(IndexView.class::cast).collect(Collectors.toList()));
  134. this.module = unit.getAttachment(Attachments.MODULE);
  135. this.metaData = unit.getAttachment(WarMetaData.ATTACHMENT_KEY).getMergedJBossWebMetaData();
  136. this.config = config;
  137. this.host = new ServiceSupplierDependency<>(this.getHostServiceName());
  138. this.info = new ServiceSupplierDependency<>(UndertowService.deploymentServiceName(unit.getServiceName()).append(UndertowDeploymentInfoService.SERVICE_NAME));
  139. if (!this.getPath().equals(DEFAULT_PATH)) {
  140. MicroProfileOpenAPILogger.LOGGER.nonStandardEndpoint(unit.getName(), this.getPath(), DEFAULT_PATH);
  141. }
  142. }
  143. @Override
  144. public ServiceBuilder<?> build(ServiceTarget target) {
  145. ServiceName name = this.getServiceName();
  146. ServiceBuilder<?> builder = target.addService(name);
  147. Consumer<OpenAPI> model = new CompositeDependency(this.host, this.info).register(builder).provides(name);
  148. Service service = new FunctionalService<>(model, Function.identity(), this);
  149. return builder.setInstance(service);
  150. }
  151. @Override
  152. public OpenAPI get() {
  153. OpenApiConfig config = new OpenApiConfigImpl(this.config);
  154. IndexView indexView = new FilteredIndexView(this.index, config);
  155. OpenAPIDocumentBuilder builder = new OpenAPIDocumentBuilder();
  156. builder.config(config);
  157. Map.Entry<VirtualFile, Format> entry = findStaticFile(this.root);
  158. if (entry != null) {
  159. VirtualFile file = entry.getKey();
  160. Format format = entry.getValue();
  161. try (OpenApiStaticFile staticFile = new OpenApiStaticFile(file.openStream(), format)) {
  162. builder.staticFileModel(OpenApiProcessor.modelFromStaticFile(staticFile));
  163. } catch (IOException e) {
  164. throw MicroProfileOpenAPILogger.LOGGER.failedToLoadStaticFile(e, file.getPathNameRelativeTo(this.root), this.deploymentName);
  165. }
  166. }
  167. builder.annotationsModel(OpenApiProcessor.modelFromAnnotations(config, AnnotationScanner.class.getClassLoader(), indexView));
  168. builder.readerModel(OpenApiProcessor.modelFromReader(config, this.module.getClassLoader()));
  169. builder.filter(OpenApiProcessor.getFilter(config, this.module.getClassLoader()));
  170. OpenAPI model = builder.build();
  171. // Generate default title and description based on web metadata
  172. DescriptionGroupMetaData descriptionMetaData = this.metaData.getDescriptionGroup();
  173. String displayName = (descriptionMetaData != null) ? descriptionMetaData.getDisplayName() : null;
  174. String title = (displayName != null) ? displayName : this.deploymentName;
  175. String description = (descriptionMetaData != null) ? descriptionMetaData.getDescription() : null;
  176. Info info = model.getInfo();
  177. // Override SmallRye's default title
  178. if (info.getTitle().equals(DEFAULT_TITLE)) {
  179. info.setTitle(title);
  180. }
  181. if (info.getDescription() == null) {
  182. info.setDescription(description);
  183. }
  184. Host host = this.host.get();
  185. List<UndertowListener> listeners = host.getServer().getListeners();
  186. if (model.getServers() == null) {
  187. // Generate Server entries if none exist
  188. String contextPath = this.info.get().getContextPath();
  189. if (this.config.getOptionalValue(RELATIVE_SERVER_URLS, Boolean.class).orElse(Boolean.TRUE).booleanValue()) {
  190. model.setServers(Collections.singletonList(OASFactory.createServer().url(contextPath)));
  191. } else {
  192. int aliases = host.getAllAliases().size();
  193. int size = 0;
  194. for (UndertowListener listener : listeners) {
  195. size += aliases + listener.getSocketBinding().getClientMappings().size();
  196. }
  197. List<Server> servers = new ArrayList<>(size);
  198. for (UndertowListener listener : listeners) {
  199. SocketBinding binding = listener.getSocketBinding();
  200. Set<String> virtualHosts = new TreeSet<>(host.getAllAliases());
  201. // The name of the host is not a real virtual host (e.g. default-host)
  202. virtualHosts.remove(host.getName());
  203. InetAddress address = binding.getAddress();
  204. // Omit wildcard addresses
  205. if (!address.isAnyLocalAddress()) {
  206. virtualHosts.add(address.getCanonicalHostName());
  207. }
  208. for (String virtualHost : virtualHosts) {
  209. Server server = createServer(listener.getProtocol(), virtualHost, binding.getPort(), contextPath);
  210. if (server != null) {
  211. servers.add(server);
  212. }
  213. }
  214. for (ClientMapping mapping : binding.getClientMappings()) {
  215. Server server = createServer(listener.getProtocol(), mapping.getDestinationAddress(), mapping.getDestinationPort(), contextPath);
  216. if (server != null) {
  217. servers.add(server);
  218. }
  219. }
  220. }
  221. model.setServers(servers);
  222. }
  223. }
  224. if (listeners.stream().map(UndertowListener::getProtocol).noneMatch(REQUISITE_LISTENERS::contains)) {
  225. LOGGER.requiredListenersNotFound(host.getServer().getName(), REQUISITE_LISTENERS);
  226. }
  227. return model;
  228. }
  229. private static Map.Entry<VirtualFile, Format> findStaticFile(VirtualFile root) {
  230. // Format search order
  231. for (Format format : EnumSet.of(Format.YAML, Format.JSON)) {
  232. for (String resource : STATIC_FILES.get(format)) {
  233. VirtualFile file = root.getChild(resource);
  234. if (file.exists()) {
  235. return new AbstractMap.SimpleImmutableEntry<>(file, format);
  236. }
  237. }
  238. }
  239. return null;
  240. }
  241. private static Server createServer(String protocol, String host, int port, String path) {
  242. try {
  243. URL url = new URL(protocol, host, port, path);
  244. if (port == url.getDefaultPort()) {
  245. url = new URL(protocol, host, path);
  246. }
  247. return OASFactory.createServer().url(url.toString());
  248. } catch (MalformedURLException e) {
  249. // Skip listeners with no known URLStreamHandler (e.g. AJP)
  250. return null;
  251. }
  252. }
  253. }