All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fish.payara.microprofile.openapi.impl.OpenAPISupplier Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2020-2024 Payara Foundation and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://github.com/payara/Payara/blob/master/LICENSE.txt
 * See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * The Payara Foundation designates this particular file as subject to the "Classpath"
 * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package fish.payara.microprofile.openapi.impl;

import static java.util.stream.Collectors.toSet;

import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Logger;

import com.sun.enterprise.v3.server.ApplicationLifecycle;
import com.sun.enterprise.v3.services.impl.GrizzlyService;

import fish.payara.microprofile.openapi.impl.model.util.ModelUtils;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.glassfish.api.admin.ServerEnvironment;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.glassfish.grizzly.config.dom.NetworkListener;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.hk2.classmodel.reflect.Parser;
import org.glassfish.hk2.classmodel.reflect.Type;
import org.glassfish.hk2.classmodel.reflect.Types;
import org.glassfish.internal.api.Globals;
import org.glassfish.internal.api.ServerContext;
import org.glassfish.internal.deployment.analysis.StructuredDeploymentTracing;

import fish.payara.microprofile.openapi.impl.config.OpenApiConfiguration;
import fish.payara.microprofile.openapi.impl.model.OpenAPIImpl;
import fish.payara.microprofile.openapi.impl.processor.ApplicationProcessor;
import fish.payara.microprofile.openapi.impl.processor.BaseProcessor;
import fish.payara.microprofile.openapi.impl.processor.ConfigPropertyProcessor;
import fish.payara.microprofile.openapi.impl.processor.FileProcessor;
import fish.payara.microprofile.openapi.impl.processor.FilterProcessor;
import fish.payara.microprofile.openapi.impl.processor.ModelReaderProcessor;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;

public class OpenAPISupplier implements Supplier {

    private static final Logger logger = Logger.getLogger(OpenAPISupplier.class.getName());

    private final OpenApiConfiguration config;
    private final String applicationId;
    private final String contextRoot;
    private final ReadableArchive archive;
    private final ClassLoader classLoader;

    private volatile OpenAPI document;

    private boolean enabled;

    public OpenAPISupplier(String applicationId, String contextRoot,
            ReadableArchive archive, ClassLoader classLoader) {
        this.config = new OpenApiConfiguration(classLoader);
        this.applicationId = applicationId;
        this.contextRoot = contextRoot;
        this.archive = archive;
        this.classLoader = classLoader;
        this.enabled = true;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public synchronized OpenAPI get() {
        if (this.document != null) {
            return this.document;
        }
        if (!enabled) {
            return null;
        }

        try {
            // collect types from this WAR project, including WEB-INF/lib jars
            Map types = collectTypesFromArchive(archive, applicationId);

            // if configured to scan libs, scan libs of the parent EAR (if available)
            if (config != null && config.getScanLib()) {
                Map earLibTypes = collectEarLibTypes(archive.getParentArchive());
                types.putAll(earLibTypes);
            }

            OpenAPI doc = new OpenAPIImpl();
            try {
                final List baseURLs = getServerURL(contextRoot);
                doc = new ConfigPropertyProcessor().process(doc, config);
                doc = new ModelReaderProcessor().process(doc, config);
                doc = new FileProcessor(classLoader).process(doc, config);
                doc = new ApplicationProcessor(
                        types,
                        filterTypes(archive, types, config != null && config.getScanLib()),
                        filterTypes(archive, types, false),
                        classLoader
                ).process(doc, config);
                if (doc.getPaths() != null && !doc.getPaths().getPathItems().isEmpty()) {
                    ((OpenAPIImpl) doc).setEndpoints(ModelUtils.buildEndpoints(null, contextRoot,
                            doc.getPaths().getPathItems().keySet()));
                }
                doc = new BaseProcessor(baseURLs).process(doc, config);
                doc = new FilterProcessor().process(doc, config);
            } finally {
                this.document = doc;
            }

            return this.document;
        } catch (Exception ex) {
            throw new RuntimeException("An error occurred while creating the OpenAPI document.", ex);
        }
    }

    private Map collectEarLibTypes(ReadableArchive parentArchive) {
        Map earLibTypes = new HashMap<>();
        if (parentArchive != null) {
            Enumeration entries = parentArchive.entries();
            while (entries.hasMoreElements()) {
                String entry = entries.nextElement();
                // scan only lib/*.jar libraries
                if (entry.startsWith("lib/") && entry.endsWith(".jar")) {
                    try {
                        Map libTypes = collectTypesFromArchive(parentArchive.getSubArchive(entry),
                                applicationId + "-parent-" + entry);
                        earLibTypes.putAll(libTypes);
                    } catch (IOException ex) {
                        logger.log(Level.SEVERE, "Unable to parse EAR archive '" + entry + "': " + ex.getMessage(), ex);
                    }
                }
            }
        }
        return earLibTypes;
    }

    private Map collectTypesFromArchive(ReadableArchive archive, String entryId) throws IOException {
        Map types = new HashMap<>();
        Parser earLibParser;
        earLibParser = Globals.get(ApplicationLifecycle.class).getDeployableParser(archive,
                true,
                true,
                StructuredDeploymentTracing.create(entryId),
                logger
        );
        types.putAll(typesToMap(earLibParser.getContext().getTypes(), archive.getURI()));
        return types;
    }

    public static Map typesToMap(Types types, URI archive) {
        return types.getAllTypes().stream()
                // We only care about classes defined by the application. With the switch to OSGi R8 and allowing Felix
                // to import/export JDK classes we need to filter out said JDK classes as (currently) HK2 does not do
                // so for us. Collecting to a map based on name without filtering would lead to a conflict between
                // the ClassModel and the 'xType' e.g. the ClassModel for the 'Enum' class and the 'EnumType'
                // used for modelling things of type 'Enum' would both have the name 'java.lang.Enum'
                .filter(type -> type.getDefiningURIs().stream().anyMatch(
                        definingUri -> definingUri.getPath().contains(archive.getPath())))
                .collect(Collectors.toMap((t) -> t.getName(), Function.identity(), (first, second) -> {
                    logger.log(Level.FINE, "Duplicate type {0} detected while performing OpenAPI scanning, will use the first.",
                            first.getName());
                    logger.log(Level.FINER, "First duplicate type: {0}\n\nSecond duplicate type: {1}",
                            new Object[]{first.toString(), second.toString()});
                    return first;
                }));
    }

    /**
     * @return a list of all classes in the archive.
     */
    private Set filterTypes(ReadableArchive archive, Map hk2Types, boolean scanLibs) {
        Set types = new HashSet<>();
        types.addAll(filterLibTypes(hk2Types, archive, scanLibs));
        types.addAll(
                Collections.list(archive.entries()).stream()
                        // Only use the classes
                        .filter(clazz -> clazz.endsWith(".class"))
                        // Remove the WEB-INF/classes and return the proper class name format
                        .map(clazz -> clazz.replaceAll("WEB-INF/classes/", "").replace("/", ".").replace(".class", ""))
                        // Fetch class type
                        .map(clazz -> hk2Types.get(clazz))
                        // Don't return null classes
                        .filter(Objects::nonNull)
                        .collect(toSet())
        );
        return config == null ? types : config.getValidClasses(types);
    }

    private Set filterLibTypes(
            Map hk2Types,
            ReadableArchive archive,
            boolean scanLibs
    ) {
        Set types = new HashSet<>();
        if (scanLibs) {
            // add libraries from WAR's /WEB-INF/lib/*.jar
            addFoundClasses(archive, hk2Types, "WEB-INF/lib/", types);

            // add here also EAR's /lib/*.jar
            addFoundClasses(archive.getParentArchive(), hk2Types, "lib/", types);
        }
        return types;
    }

    private void addFoundClasses(ReadableArchive archiveToScan, Map hk2Types, String prefix, Set types) {
        if (archiveToScan != null) {
            Enumeration subArchiveItr = archiveToScan.entries();
            while (subArchiveItr.hasMoreElements()) {
                String subArchiveName = subArchiveItr.nextElement();
                if (subArchiveName.startsWith(prefix) && subArchiveName.endsWith(".jar")) {
                    try {
                        ReadableArchive subArchive = archiveToScan.getSubArchive(subArchiveName);
                        types.addAll(
                                Collections.list(subArchive.entries())
                                        .stream()
                                        // Only use the classes
                                        .filter(clazz -> clazz.endsWith(".class"))
                                        // return the proper class name format
                                        .map(clazz -> clazz.replace("/", ".").replace(".class", ""))
                                        // Fetch class type
                                        .map(clazz -> hk2Types.get(clazz))
                                        // Don't return null classes
                                        .filter(Objects::nonNull)
                                        .collect(toSet())
                        );
                    } catch (IOException ex) {
                        throw new IllegalStateException(ex);
                    }
                }
            }
        }
    }

    private List getServerURL(String contextRoot) {
        List result = new ArrayList<>();
        ServerContext context = Globals.get(ServerContext.class);

        String hostName;
        try {
            hostName = InetAddress.getLocalHost().getCanonicalHostName();
        } catch (UnknownHostException ex) {
            hostName = "localhost";
        }

        String instanceType = Globals.get(ServerEnvironment.class).getRuntimeType().toString();
        List httpPorts = new ArrayList<>();
        List httpsPorts = new ArrayList<>();
        List networkListeners = context.getConfigBean().getConfig().getNetworkConfig().getNetworkListeners().getNetworkListener();
        String adminListener = context.getConfigBean().getConfig().getAdminListener().getName();
        networkListeners
                .stream()
                .filter(networkListener -> Boolean.parseBoolean(networkListener.getEnabled()))
                .forEach(networkListener -> {

                    int port;
                    try {
                        // get the dynamic config port
                        port = Globals.get(GrizzlyService.class).getRealPort(networkListener);
                    } catch (MultiException ex) {
                        // get the port in the domain xml
                        port = Integer.parseInt(networkListener.getPort());
                    }

                    // Check if this listener is using HTTP or HTTPS
                    boolean securityEnabled = Boolean.parseBoolean(networkListener.findProtocol().getSecurityEnabled());
                    List ports = securityEnabled ? httpsPorts : httpPorts;

                    // If this listener isn't the admin listener, it must be an HTTP/HTTPS listener
                    if (!networkListener.getName().equals(adminListener)) {
                        ports.add(port);
                    } else if (instanceType.equals("MICRO")) {
                        // micro instances can use the admin listener as both an admin and HTTP/HTTPS port
                        ports.add(port);
                    }
                });

        for (Integer httpPort : httpPorts) {
            try {
                result.add(new URL("http", hostName, httpPort, contextRoot));
            } catch (MalformedURLException ex) {
                // ignore
            }
        }
        for (Integer httpsPort : httpsPorts) {
            try {
                result.add(new URL("https", hostName, httpsPort, contextRoot));
            } catch (MalformedURLException ex) {
                // ignore
            }
        }
        return result;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy