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

com.tinkerpop.gremlin.console.commands.InstallCommand.groovy Maven / Gradle / Ivy

The newest version!
package com.tinkerpop.gremlin.console.commands

import com.tinkerpop.gremlin.console.Mediator
import com.tinkerpop.gremlin.console.plugin.PluggedIn
import com.tinkerpop.gremlin.groovy.plugin.Artifact
import com.tinkerpop.gremlin.groovy.plugin.GremlinPlugin
import groovy.grape.Grape
import org.codehaus.groovy.tools.shell.CommandSupport
import org.codehaus.groovy.tools.shell.Groovysh

import java.nio.file.*
import java.util.jar.JarFile
import java.util.jar.Manifest

/**
 * Install a dependency into the console.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
class InstallCommand extends CommandSupport {

    private final static String fileSep = System.getProperty("file.separator")
    private final Mediator mediator

    public InstallCommand(final Groovysh shell, final Mediator mediator) {
        super(shell, ":install", ":+")
        this.mediator = mediator
    }

    @Override
    def Object execute(final List arguments) {
        final def artifact = createArtifact(arguments)
        final def dep = makeDepsMap(artifact)
        final def pluginsThatNeedRestart = grabDeps(dep)

        final String extClassPath = getPathFromDependency(dep)
        final File f = new File(extClassPath)
        if (f.exists())
            return "A module with the name ${dep.module} is already installed"
        else {
            f.mkdirs()
            new File(extClassPath + fileSep + "plugin-info.txt").withWriter { out -> out << arguments.join(":") }
        }

        final def dependencyLocations = Grape.resolve([classLoader: shell.getInterp().getClassLoader()], null, dep)

        def fs = FileSystems.default
        def target = fs.getPath(extClassPath)

        // collect the files already on the path in /lib. making some unfortunate assumptions about what the path
        // looks like for the gremlin distribution
        def filesAlreadyInPath = []
        def libClassPath
        try {
            libClassPath = fs.getPath(System.getProperty("user.dir") + fileSep + "lib")
            getFileNames(filesAlreadyInPath, libClassPath)
        } catch (Exception ignored) {
            // the user might have a non-standard directory system.  if they are non-standard then they must be
            // smart and they are therefore capable of resolving their own dependency problems.  this could also
            // mean that they are running gremlin from source and not from target/*standalone*
            io.println "Detected a non-standard Gremlin directory structure during install.  Expecting a 'lib' " +
                    "directory sibling to 'ext'. This message does not necessarily imply failure, however " +
                    "the console requires a certain directory structure for proper execution. Altering that " +
                    "structure can lead to unexpected behavior."
        }

        // ignore slf4j related jars.  they are already in the path and will create duplicate bindings which
        // generate annoying log messages that make you think stuff is wrong.  also, don't bring over files
        // that are already on the path
        dependencyLocations.collect { fs.getPath(it.path) }
                .findAll { !(it.fileName.toFile().name ==~ /(slf4j|logback\-classic)-.*\.jar/) }
                .findAll {
            !filesAlreadyInPath.collect { it.getFileName().toString() }.contains(it.fileName.toFile().name)
        }
                .each { Files.copy(it, target.resolve(it.fileName), StandardCopyOption.REPLACE_EXISTING) }

        // additional dependencies are outside those pulled by grape and are defined in the manifest of the plugin jar.
        // if a plugin uses that setting, it should force "restart" when the plugin is activated.  right now,
        // it is up to the plugin developer to enforce that setting.
        getAdditionalDependencies(target, artifact).collect { fs.getPath(it.path) }
                .findAll { !(it.fileName.toFile().name ==~ /(slf4j|logback\-classic)-.*\.jar/) }
                .findAll {
            !filesAlreadyInPath.collect { it.getFileName().toString() }.contains(it.fileName.toFile().name)
        }
                .each { Files.copy(it, target.resolve(it.fileName), StandardCopyOption.REPLACE_EXISTING) }

        // the ordering of jars seems to matter in some cases (e.g. neo4j).  the plugin system allows the plugin
        // to place a Gremlin-Plugin entry in the jar manifest file to define where specific jar files should
        // go in the path which provides enough flexibility to control when jars should load.  unfortunately,
        // this "ordering" issue doesn't seem to be documented as an issue anywhere and it is difficult to say
        // whether it is a java issue, groovy classloader issue, grape issue, etc.  see this issue for more
        // on the weirdness: https://github.com/tinkerpop/tinkerpop3/issues/230
        //
        // another unfortunate side-effect to this approach is that manual cleanup of jars is kinda messy now
        // because you can't just delete the plugin director as one or more of the jars might have been moved.
        // unsure of what the long term effects of this is.  at the end of the day, users may simply need to
        // know something about their dependencies in order to have lots of "installed" plugins/dependencies.
        alterPaths(target, artifact)

        return "Loaded: " + arguments + (pluginsThatNeedRestart.size() == 0 ? "" : " - restart the console to use $pluginsThatNeedRestart")
    }

    private static String getPathFromDependency(final Map dep) {
        return System.getProperty("user.dir") + fileSep + "ext" + fileSep + (String) dep.module
    }

    private static alterPaths(final Path extPath, final Artifact artifact) {
        try {
            // another assumption about the pathing - seems safe for right now as the :install command is
            // responsible for all this stuff.  if the user chooses to manually install their dependencies
            // to the console, then it's up to them to sort this stuff out.
            def pathToInstalled = extPath.resolve(artifact.artifact + "-" + artifact.version + ".jar")
            final JarFile jar = new JarFile(pathToInstalled.toFile());
            final Manifest manifest = jar.getManifest()

            // containsKey doesn't seem to want to work - so just check for null - dah
            def attrLine = manifest.mainAttributes.getValue("Gremlin-Plugin-Paths")
            if (attrLine != null) {
                def splitLine = attrLine.split(";")
                splitLine.each {
                    def kv = it.split("=")
                    Files.move(extPath.resolve(kv[0]), extPath.resolve(kv[1]), StandardCopyOption.REPLACE_EXISTING)
                }
            }
        } catch (Exception ex) {
            // errors here will likely have to do with bad pathing or poorly constructed entries in the manifest.
            // hopefully these will only occur for developers of plugins who need to make use of this function.
            // internally to tinkerpop this is a neo4j-only issue
            throw new RuntimeException(ex)
        }
    }

    private Set getAdditionalDependencies(final Path extPath, final Artifact artifact) {
        try {
            def pathToInstalled = extPath.resolve(artifact.artifact + "-" + artifact.version + ".jar")
            final JarFile jar = new JarFile(pathToInstalled.toFile())
            final Manifest manifest = jar.getManifest()
            def attrLine = manifest.mainAttributes.getValue("Gremlin-Plugin-Dependencies")
            def additionalDependencies = [] as Set
            if (attrLine != null) {
                def splitLine = attrLine.split(";")
                splitLine.each {
                    def artifactBits = it.split(":")
                    def additional = new Artifact(artifactBits[0], artifactBits[1], artifactBits[2])

                    final def additionalDep = makeDepsMap(additional)
                    additionalDependencies.addAll(Grape.resolve([classLoader: shell.getInterp().getClassLoader()], null, additionalDep))
                }
            }

            return additionalDependencies
        } catch (Exception ex) {
            throw new RuntimeException(ex)
        }
    }

    private def grabDeps(final Map map) {
        Grape.grab(map)

        def pluginsThatNeedRestart = [] as Set

        // note that the service loader utilized the classloader from the groovy shell as shell class are available
        // from within there given loading through Grape.
        ServiceLoader.load(GremlinPlugin.class, shell.getInterp().getClassLoader()).forEach { plugin ->
            if (!mediator.availablePlugins.containsKey(plugin.class.name)) {
                mediator.availablePlugins.put(plugin.class.name, new PluggedIn(plugin, shell, io, false))
                if (plugin.requireRestart())
                    pluginsThatNeedRestart << plugin.name
            }
        }

        return pluginsThatNeedRestart
    }

    private static def createArtifact(final List arguments) {
        final String group = arguments.size() >= 1 ? arguments.get(0) : null
        final String module = arguments.size() >= 2 ? arguments.get(1) : null
        final String version = arguments.size() >= 3 ? arguments.get(2) : null

        if (group == null || group.isEmpty())
            throw new IllegalArgumentException("Group cannot be null or empty")

        if (module == null || module.isEmpty())
            throw new IllegalArgumentException("Module cannot be null or empty")

        if (version == null || version.isEmpty())
            throw new IllegalArgumentException("Version cannot be null or empty")

        return new Artifact(group, module, version)
    }

    private def makeDepsMap(final Artifact artifact) {
        final Map map = new HashMap<>()
        map.put("classLoader", shell.getInterp().getClassLoader())
        map.put("group", artifact.getGroup())
        map.put("module", artifact.getArtifact())
        map.put("version", artifact.getVersion())
        map.put("changing", false)
        return map
    }

    private static void getFileNames(final List fileNames, final Path dir) {
        final DirectoryStream stream = Files.newDirectoryStream(dir)
        for (Path path : stream) {
            if (path.toFile().isDirectory()) getFileNames(fileNames, path)
            else {
                fileNames.add(path.toAbsolutePath())
            }
        }
        stream.close()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy