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

com.seeq.link.agent.DefaultConnectorLoader Maven / Gradle / Ivy

There is a newer version: 66.0.0-v202410141803
Show newest version
package com.seeq.link.agent;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.seeq.link.agent.interfaces.ConnectorLoader;
import com.seeq.link.sdk.ConnectorV2Host;
import com.seeq.link.sdk.interfaces.Connector;
import com.seeq.link.sdk.interfaces.ConnectorV2;
import com.seeq.link.sdk.utilities.FileSystemGlob;

import lombok.extern.slf4j.Slf4j;

/**
 * Loads any JARs found on its ConnectorSearchPaths properties that have classes that implement the {@link Connector}
 * interface.
 */
@Slf4j
public class DefaultConnectorLoader implements ConnectorLoader {

    /**
     * A list of file paths with a terminating file pattern that dictates where the loader will search for JARs.
     *
     * For example: ../../../seeq-link-xhq-connector/*-connector.jar
     *
     * Relative and absolute file paths can be supplied. If relative, the folder that the agent JAR is located in is
     * used as the root.
     */
    private String[] connectorSearchPaths;

    /**
     * A shared {@link URLClassLoader} instance to use for non-isolated classloading. This is required in Java 11
     * because the system classloader can no longer be cast to URLClassLoader.
     */
    private URLClassLoader sharedClassLoader;

    public String[] getConnectorSearchPaths() {
        return this.connectorSearchPaths;
    }

    @Override
    public void initialize(String[] connectorSearchPaths, URLClassLoader sharedClassLoader) {
        this.connectorSearchPaths = connectorSearchPaths;
        this.sharedClassLoader = sharedClassLoader;
    }

    public interface ClassEnumerator {
        String nextElement();

        void close();
    }

    public static class DirectoryClassEnumerator implements ClassEnumerator {
        private final Deque indexStack = new ArrayDeque<>();
        private final Deque filesStack = new ArrayDeque<>();
        private Integer currentIndex;
        private File[] currentFiles;

        public DirectoryClassEnumerator(File directory) {
            this.currentIndex = 0;
            this.currentFiles = directory.listFiles();
        }

        @Override
        public String nextElement() {
            while (true) {
                if (this.currentIndex >= this.currentFiles.length) {
                    if (this.indexStack.peek() == null) {
                        return null;
                    }
                    this.currentIndex = this.indexStack.pop();
                    this.currentFiles = this.filesStack.pop();

                    this.currentIndex++;
                } else {
                    File file = this.currentFiles[this.currentIndex];
                    if (file.isDirectory()) {
                        this.indexStack.push(this.currentIndex);
                        this.filesStack.push(this.currentFiles);
                        this.currentIndex = 0;
                        this.currentFiles = file.listFiles();
                    } else if (file.getName().endsWith(".class")) {
                        String pkg = "";
                        Iterator itIndexStack = this.indexStack.descendingIterator();
                        Iterator itFilesStack = this.filesStack.descendingIterator();
                        while (itIndexStack.hasNext()) {
                            Integer pkgIndex = itIndexStack.next();
                            File[] pkgFiles = itFilesStack.next();
                            pkg += pkgFiles[pkgIndex].getName() + ".";
                        }

                        this.currentIndex++;

                        return pkg + file.getName().replace(".class", "");
                    } else {
                        this.currentIndex++;
                    }
                }
            }
        }

        @Override
        public void close() {
            // Nothing to do
        }
    }

    public static class JarClassEnumerator implements ClassEnumerator {
        private JarFile jarFile = null;
        private Iterator jarFileStreamIterator = null;

        public JarClassEnumerator(File jarFilePath) {
            try {
                this.jarFile = new JarFile(jarFilePath);
                this.jarFileStreamIterator = this.jarFile.stream().iterator();
            } catch (IOException e) {
                LOG.error("Could not open JAR file \"{}\"", jarFilePath, e);
            }
        }

        @Override
        public String nextElement() {
            if (this.jarFileStreamIterator == null) {
                return null;
            }

            while (this.jarFileStreamIterator.hasNext()) {
                JarEntry jarEntry = this.jarFileStreamIterator.next();
                if (jarEntry.getName().endsWith(".class")) {
                    String name = jarEntry.getName();
                    String className = name.replaceAll("/", ".").replace(".class", "");

                    return className;
                }
            }

            return null;
        }

        @Override
        public void close() {
            try {
                this.jarFile.close();
            } catch (IOException e) {
                // Nothing we can do
                LOG.error("Exception closing log file \"{}\"", this.jarFile.getName(), e);
            }
        }
    }

    @VisibleForTesting
    List loadConnectorJARs() {
        return this.loadConnectorJARs(true);
    }

    /**
     * Loads any JARs found on its ConnectorSearchPaths property that have classes that implement the {@link Connector}
     * interface, instantiates those objects and returns a list filled with them.
     *
     * @param isolateClassLoaders
     *         true if each connector should be loaded with its own ClassLoader
     * @return A list filled with objects that implement {@link Connector}.
     */
    @Override
    public List loadConnectorJARs(boolean isolateClassLoaders) {
        // Grab the full path of the Agent JAR that is currently executing
        Path executingAssemblyLocation;
        try {
            executingAssemblyLocation = Paths.get(DefaultConnectorLoader.class.getProtectionDomain().getCodeSource()
                    .getLocation().toURI());
        } catch (URISyntaxException e) {
            throw new IllegalStateException("Could not determine executing assembly location", e);
        }

        Path searchRootPath = executingAssemblyLocation.getParent();
        LOG.info("Search root path: '{}'", searchRootPath);

        List connectors = new ArrayList<>();
        for (String searchPath : this.connectorSearchPaths) {
            LOG.info("Searching for connectors using path: '{}'", searchPath);

            List connectorJARs = FileSystemGlob.find(searchPath, searchRootPath);

            // SQL 2.0 depends on SQL 1.0. Sort the connectors so "sql" is loaded before "sql2".
            // If we don't do this we'll get an error: CRAB-12136.
            // Loading JARs in a well-defined order should also make runtime behavior more predictable.
            //
            // CRAB-13850: Whether an OS uses \ or / in the path will affect the sorting order.
            // OS independent java sorting order is: /, numbers, \ which means the paths sort as
            // sql/, sql2/, sql2\, sql\
            // Therefore, replace \ with / before sorting so the order is always the same.
            Comparator pathComparator =
                    Comparator.comparing(s -> s.toString().toLowerCase().replace("\\", "/"));

            connectorJARs = connectorJARs.stream()
                    .sorted(pathComparator)
                    .collect(ImmutableList.toImmutableList());

            boolean atLeastOneJarFound = false;
            for (Path filePath : connectorJARs) {
                atLeastOneJarFound = true;

                LOG.info("Loading candidate JAR: '{}'", filePath);

                List classPathUrls = new ArrayList<>();

                try {
                    classPathUrls.add(filePath.toFile().toURI().toURL());

                    if (filePath.toFile().isFile()) {
                        this.addLibFolder("lib", filePath, classPathUrls);
                        this.addLibFolder("nonDistributedLib", filePath, classPathUrls);
                    }
                } catch (Exception e) {
                    LOG.error("Could not add jar and dependencies to class path for '{}'", filePath, e);
                    continue;
                }
                boolean atLeastOneTypeFound;
                try {
                    URLClassLoader classLoaderForConnectors;

                    if (isolateClassLoaders) {
                        // Create an isolated class loader so that each connector can load different versions of
                        // their dependencies.
                        classLoaderForConnectors = new URLClassLoader(classPathUrls.toArray(new URL[0]),
                                this.sharedClassLoader);
                    } else {
                        classLoaderForConnectors = this.sharedClassLoader;
                        for (URL url : classPathUrls) {
                            ClassPathUtilities.addToClassPath(classLoaderForConnectors, url);
                        }
                    }

                    atLeastOneTypeFound = this.searchForConnectors(filePath, classLoaderForConnectors, connectors);
                } catch (Throwable e) {
                    LOG.error("Error loading connector '{}'", filePath, e);
                    continue;
                }

                if (!atLeastOneTypeFound) {
                    LOG.info("No types found in JAR '{}' that implement Connector interface", filePath);
                }
            }

            if (!atLeastOneJarFound) {
                LOG.info("No matching JARs found on search path '{}'", searchPath);
            }
        }

        return connectors;
    }

    private void addLibFolder(String name, Path filePath, List classPathUrls) throws Exception {
        Path libFolder = filePath.getParent().resolve(name);
        if (libFolder.toFile().exists()) {
            LOG.info("lib folder '{}' found, adding JARs to classpath", libFolder);
            classPathUrls.addAll(ClassPathUtilities.getJarsInFolder(libFolder.toFile()));
        }
    }

    private boolean searchForConnectors(Path filePath, ClassLoader classLoaderForConnectors, List connectors)
            throws IllegalAccessException, ClassNotFoundException {
        boolean atLeastOneTypeFound = false;
        for (Connector connector : ServiceLoader.load(Connector.class, classLoaderForConnectors)) {
            atLeastOneTypeFound |= this.maybeAddConnector(connector, connectors);
        }
        for (ConnectorV2 connector : ServiceLoader.load(ConnectorV2.class, classLoaderForConnectors)) {
            atLeastOneTypeFound |= this.maybeAddConnector(new ConnectorV2Host(connector), connectors);
        }
        if (atLeastOneTypeFound) {
            return true;
        } else {
            return this.legacySearch(filePath, classLoaderForConnectors, connectors);
        }
    }

    private boolean legacySearch(Path filePath, ClassLoader classLoaderForConnectors, List connectors)
            throws ClassNotFoundException, IllegalAccessException {
        boolean atLeastOneTypeFound = false;
        ClassEnumerator enumerator;
        if (filePath.toFile().isFile()) {
            enumerator = new JarClassEnumerator(filePath.toFile());
        } else {
            enumerator = new DirectoryClassEnumerator(filePath.toFile());
        }
        while (true) {
            String className = enumerator.nextElement();
            if (className == null) {
                break;
            }

            Class type = Class.forName(className, true, classLoaderForConnectors);

            if (Modifier.isAbstract(type.getModifiers())) {
                continue;
            }

            Connector connector = null;
            List> interfaces = Arrays.asList(type.getInterfaces());
            if (interfaces.contains(Connector.class)) {
                try {
                    connector = (Connector) type.newInstance();
                } catch (InstantiationException e) {
                    LOG.error("Could not instantiate class '{}', exception:\n", className, e);
                    continue;
                }
            } else if (interfaces.contains(ConnectorV2.class)) {
                ConnectorV2 connectorV2;
                try {
                    connectorV2 = (ConnectorV2) type.newInstance();
                } catch (InstantiationException e) {
                    LOG.error("Could not instantiate class '{}', exception:\n", className, e);
                    continue;
                }

                connector = new ConnectorV2Host(connectorV2);
            }

            if (connector != null) {
                atLeastOneTypeFound |= this.maybeAddConnector(connector, connectors);

            }
        }
        enumerator.close();
        return atLeastOneTypeFound;
    }

    private boolean maybeAddConnector(Connector connector, List connectors) {
        boolean alreadyLoaded = false;

        for (Connector existingConnectorType : connectors) {
            if (existingConnectorType.getName().equals(connector.getName())) {
                LOG.info("Connector '{}' already loaded", connector.getName());
                alreadyLoaded = true;
                break;
            }
        }

        if (!alreadyLoaded) {
            connectors.add(connector);
            LOG.info("Successfully loaded connector '{}'", connector.getName());
        }
        return !alreadyLoaded;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy