/microprofile/openapi-smallrye/src/main/java/org/wildfly/extension/microprofile/openapi/deployment/OpenAPIModelServiceConfigurator.java
Java | 280 lines | 216 code | 27 blank | 37 comment | 30 complexity | 8a9fa9f5111d8d503ebc94b3071e1384 MD5 | raw file
- /*
- * JBoss, Home of Professional Open Source.
- * Copyright 2019, Red Hat, Inc., and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors.
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
- package org.wildfly.extension.microprofile.openapi.deployment;
- import static org.wildfly.extension.microprofile.openapi.logging.MicroProfileOpenAPILogger.LOGGER;
- import java.io.IOException;
- import java.net.InetAddress;
- import java.net.MalformedURLException;
- import java.net.URL;
- import java.util.AbstractMap;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.EnumMap;
- import java.util.EnumSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TreeSet;
- import java.util.function.Consumer;
- import java.util.function.Function;
- import java.util.function.Supplier;
- import java.util.stream.Collectors;
- import org.eclipse.microprofile.config.Config;
- import org.eclipse.microprofile.openapi.OASFactory;
- import org.eclipse.microprofile.openapi.models.OpenAPI;
- import org.eclipse.microprofile.openapi.models.info.Info;
- import org.eclipse.microprofile.openapi.models.servers.Server;
- import org.eclipse.microprofile.openapi.spi.OASFactoryResolver;
- import org.jboss.as.network.ClientMapping;
- import org.jboss.as.network.SocketBinding;
- import org.jboss.as.server.deployment.Attachments;
- import org.jboss.as.server.deployment.DeploymentUnit;
- import org.jboss.as.web.common.WarMetaData;
- import org.jboss.jandex.CompositeIndex;
- import org.jboss.jandex.Index;
- import org.jboss.jandex.IndexView;
- import org.jboss.metadata.javaee.spec.DescriptionGroupMetaData;
- import org.jboss.metadata.web.jboss.JBossWebMetaData;
- import org.jboss.modules.Module;
- import org.jboss.msc.Service;
- import org.jboss.msc.service.ServiceBuilder;
- import org.jboss.msc.service.ServiceName;
- import org.jboss.msc.service.ServiceTarget;
- import org.jboss.vfs.VirtualFile;
- import org.wildfly.clustering.service.CompositeDependency;
- import org.wildfly.clustering.service.FunctionalService;
- import org.wildfly.clustering.service.ServiceConfigurator;
- import org.wildfly.clustering.service.ServiceSupplierDependency;
- import org.wildfly.clustering.service.SimpleServiceNameProvider;
- import org.wildfly.clustering.service.SupplierDependency;
- import org.wildfly.extension.microprofile.openapi.logging.MicroProfileOpenAPILogger;
- import org.wildfly.extension.undertow.Capabilities;
- import org.wildfly.extension.undertow.Host;
- import org.wildfly.extension.undertow.UndertowListener;
- import org.wildfly.extension.undertow.UndertowService;
- import org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService;
- import io.smallrye.openapi.api.OpenApiConfig;
- import io.smallrye.openapi.api.OpenApiConfigImpl;
- import io.smallrye.openapi.runtime.OpenApiProcessor;
- import io.smallrye.openapi.runtime.OpenApiStaticFile;
- import io.smallrye.openapi.runtime.io.Format;
- import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
- import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner;
- import io.smallrye.openapi.spi.OASFactoryResolverImpl;
- import io.undertow.servlet.api.DeploymentInfo;
- /**
- * Configures a service that provides an OpenAPI model for a deployment.
- * @author Paul Ferraro
- */
- public class OpenAPIModelServiceConfigurator extends SimpleServiceNameProvider implements OpenAPIServiceNameProvider, ServiceConfigurator, Supplier<OpenAPI> {
- private static final String PATH = "mp.openapi.extensions.path";
- private static final String DEFAULT_PATH = "/openapi";
- private static final String RELATIVE_SERVER_URLS = "mp.openapi.extensions.servers.relative";
- private static final String DEFAULT_TITLE = "Generated API";
- private static final Set<String> REQUISITE_LISTENERS = Collections.singleton("http");
- private static final Map<Format, List<String>> STATIC_FILES = new EnumMap<>(Format.class);
- static {
- // Order resource names by search order
- STATIC_FILES.put(Format.YAML, Arrays.asList(
- "/META-INF/openapi.yaml",
- "/WEB-INF/classes/META-INF/openapi.yaml",
- "/META-INF/openapi.yml",
- "/WEB-INF/classes/META-INF/openapi.yml"));
- STATIC_FILES.put(Format.JSON, Arrays.asList(
- "/META-INF/openapi.json",
- "/WEB-INF/classes/META-INF/openapi.json"));
- // Set the static OASFactoryResolver eagerly avoiding the need perform TCCL service loading later
- OASFactoryResolver.setInstance(new OASFactoryResolverImpl());
- }
- private final Config config;
- private final String deploymentName;
- private final VirtualFile root;
- private final CompositeIndex index;
- private final Module module;
- private final JBossWebMetaData metaData;
- private final SupplierDependency<Host> host;
- private final SupplierDependency<DeploymentInfo> info;
- public OpenAPIModelServiceConfigurator(DeploymentUnit unit, String serverName, String hostName, Config config) {
- super(unit.getAttachment(Attachments.CAPABILITY_SERVICE_SUPPORT).getCapabilityServiceName(Capabilities.CAPABILITY_HOST, serverName, hostName).append(config.getOptionalValue(PATH, String.class).orElse(DEFAULT_PATH)));
- this.deploymentName = unit.getName();
- this.root = unit.getAttachment(Attachments.DEPLOYMENT_ROOT).getRoot();
- // Convert org.jboss.as.server.deployment.annotation.CompositeIndex to org.jboss.jandex.CompositeIndex
- Collection<Index> indexes = new ArrayList<>(unit.getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes());
- if (unit.getParent() != null) {
- // load all composite indexes of the parent deployment unit
- indexes.addAll(unit.getParent().getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes());
- // load all composite indexes of the parent's accessible sub deployments
- unit.getParent()
- .getAttachment(Attachments.ACCESSIBLE_SUB_DEPLOYMENTS)
- .forEach(subdeployment -> indexes.addAll(
- subdeployment.getAttachment(Attachments.COMPOSITE_ANNOTATION_INDEX).getIndexes()));
- }
- this.index = CompositeIndex.create(indexes.stream().map(IndexView.class::cast).collect(Collectors.toList()));
- this.module = unit.getAttachment(Attachments.MODULE);
- this.metaData = unit.getAttachment(WarMetaData.ATTACHMENT_KEY).getMergedJBossWebMetaData();
- this.config = config;
- this.host = new ServiceSupplierDependency<>(this.getHostServiceName());
- this.info = new ServiceSupplierDependency<>(UndertowService.deploymentServiceName(unit.getServiceName()).append(UndertowDeploymentInfoService.SERVICE_NAME));
- if (!this.getPath().equals(DEFAULT_PATH)) {
- MicroProfileOpenAPILogger.LOGGER.nonStandardEndpoint(unit.getName(), this.getPath(), DEFAULT_PATH);
- }
- }
- @Override
- public ServiceBuilder<?> build(ServiceTarget target) {
- ServiceName name = this.getServiceName();
- ServiceBuilder<?> builder = target.addService(name);
- Consumer<OpenAPI> model = new CompositeDependency(this.host, this.info).register(builder).provides(name);
- Service service = new FunctionalService<>(model, Function.identity(), this);
- return builder.setInstance(service);
- }
- @Override
- public OpenAPI get() {
- OpenApiConfig config = new OpenApiConfigImpl(this.config);
- IndexView indexView = new FilteredIndexView(this.index, config);
- OpenAPIDocumentBuilder builder = new OpenAPIDocumentBuilder();
- builder.config(config);
- Map.Entry<VirtualFile, Format> entry = findStaticFile(this.root);
- if (entry != null) {
- VirtualFile file = entry.getKey();
- Format format = entry.getValue();
- try (OpenApiStaticFile staticFile = new OpenApiStaticFile(file.openStream(), format)) {
- builder.staticFileModel(OpenApiProcessor.modelFromStaticFile(staticFile));
- } catch (IOException e) {
- throw MicroProfileOpenAPILogger.LOGGER.failedToLoadStaticFile(e, file.getPathNameRelativeTo(this.root), this.deploymentName);
- }
- }
- builder.annotationsModel(OpenApiProcessor.modelFromAnnotations(config, AnnotationScanner.class.getClassLoader(), indexView));
- builder.readerModel(OpenApiProcessor.modelFromReader(config, this.module.getClassLoader()));
- builder.filter(OpenApiProcessor.getFilter(config, this.module.getClassLoader()));
- OpenAPI model = builder.build();
- // Generate default title and description based on web metadata
- DescriptionGroupMetaData descriptionMetaData = this.metaData.getDescriptionGroup();
- String displayName = (descriptionMetaData != null) ? descriptionMetaData.getDisplayName() : null;
- String title = (displayName != null) ? displayName : this.deploymentName;
- String description = (descriptionMetaData != null) ? descriptionMetaData.getDescription() : null;
- Info info = model.getInfo();
- // Override SmallRye's default title
- if (info.getTitle().equals(DEFAULT_TITLE)) {
- info.setTitle(title);
- }
- if (info.getDescription() == null) {
- info.setDescription(description);
- }
- Host host = this.host.get();
- List<UndertowListener> listeners = host.getServer().getListeners();
- if (model.getServers() == null) {
- // Generate Server entries if none exist
- String contextPath = this.info.get().getContextPath();
- if (this.config.getOptionalValue(RELATIVE_SERVER_URLS, Boolean.class).orElse(Boolean.TRUE).booleanValue()) {
- model.setServers(Collections.singletonList(OASFactory.createServer().url(contextPath)));
- } else {
- int aliases = host.getAllAliases().size();
- int size = 0;
- for (UndertowListener listener : listeners) {
- size += aliases + listener.getSocketBinding().getClientMappings().size();
- }
- List<Server> servers = new ArrayList<>(size);
- for (UndertowListener listener : listeners) {
- SocketBinding binding = listener.getSocketBinding();
- Set<String> virtualHosts = new TreeSet<>(host.getAllAliases());
- // The name of the host is not a real virtual host (e.g. default-host)
- virtualHosts.remove(host.getName());
- InetAddress address = binding.getAddress();
- // Omit wildcard addresses
- if (!address.isAnyLocalAddress()) {
- virtualHosts.add(address.getCanonicalHostName());
- }
- for (String virtualHost : virtualHosts) {
- Server server = createServer(listener.getProtocol(), virtualHost, binding.getPort(), contextPath);
- if (server != null) {
- servers.add(server);
- }
- }
- for (ClientMapping mapping : binding.getClientMappings()) {
- Server server = createServer(listener.getProtocol(), mapping.getDestinationAddress(), mapping.getDestinationPort(), contextPath);
- if (server != null) {
- servers.add(server);
- }
- }
- }
- model.setServers(servers);
- }
- }
- if (listeners.stream().map(UndertowListener::getProtocol).noneMatch(REQUISITE_LISTENERS::contains)) {
- LOGGER.requiredListenersNotFound(host.getServer().getName(), REQUISITE_LISTENERS);
- }
- return model;
- }
- private static Map.Entry<VirtualFile, Format> findStaticFile(VirtualFile root) {
- // Format search order
- for (Format format : EnumSet.of(Format.YAML, Format.JSON)) {
- for (String resource : STATIC_FILES.get(format)) {
- VirtualFile file = root.getChild(resource);
- if (file.exists()) {
- return new AbstractMap.SimpleImmutableEntry<>(file, format);
- }
- }
- }
- return null;
- }
- private static Server createServer(String protocol, String host, int port, String path) {
- try {
- URL url = new URL(protocol, host, port, path);
- if (port == url.getDefaultPort()) {
- url = new URL(protocol, host, path);
- }
- return OASFactory.createServer().url(url.toString());
- } catch (MalformedURLException e) {
- // Skip listeners with no known URLStreamHandler (e.g. AJP)
- return null;
- }
- }
- }