From c0b685a044c3fa58cb38edb1443149194d90de7c Mon Sep 17 00:00:00 2001 From: Cedric Beust Date: Sun, 31 Jul 2016 23:11:26 -0800 Subject: [PATCH] Added command "/v1/getDependencyGraph" for the IDEA plug-in. --- .../com/beust/kobalt/internal/DynamicGraph.kt | 68 ++++++++---- .../beust/kobalt/app/remote/DependencyData.kt | 57 ++++++++-- .../app/remote/GetDependenciesCommand.kt | 1 + .../app/remote/GetDependencyGraphHandler.kt | 100 ++++++++++++++++++ .../com/beust/kobalt/app/remote/KobaltHub.kt | 19 ++-- .../com/beust/kobalt/app/remote/OldServer.kt | 1 + .../beust/kobalt/app/remote/SparkServer.kt | 2 + .../beust/kobalt/internal/DynamicGraphTest.kt | 19 ++++ 8 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 src/main/kotlin/com/beust/kobalt/app/remote/GetDependencyGraphHandler.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt index 08f4c7a0..d007ef98 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/internal/DynamicGraph.kt @@ -14,21 +14,21 @@ open class TaskResult2(success: Boolean, errorMessage: String?, val value: T) override fun toString() = com.beust.kobalt.misc.toString("TaskResult", "value", value, "success", success) } -class Node(val value: T) { - override fun hashCode() = value!!.hashCode() - override fun equals(other: Any?) : Boolean { - val result = if (other is Node<*>) other.value == value else false - return result - } - override fun toString() = value.toString() -} - class DynamicGraph { val VERBOSE = 2 val values : Collection get() = nodes.map { it.value } - val nodes = hashSetOf>() - private val dependedUpon = HashMultimap.create, Node>() - private val dependingOn = HashMultimap.create, Node>() + val nodes = hashSetOf>() + private val dependedUpon = HashMultimap.create, PrivateNode>() + private val dependingOn = HashMultimap.create, PrivateNode>() + + class PrivateNode(val value: T) { + override fun hashCode() = value!!.hashCode() + override fun equals(other: Any?) : Boolean { + val result = if (other is PrivateNode<*>) other.value == value else false + return result + } + override fun toString() = value.toString() + } companion object { fun transitiveClosure(root: T, childrenFor: (T) -> List) : List { @@ -51,22 +51,50 @@ class DynamicGraph { } return result } + + class Node(val value: T, val children: List>) { + fun dump(root : Node = this, indent: String = "") : String { + return StringBuffer().apply { + append(indent).append(root.value).append("\n") + root.children.forEach { + append(dump(it, indent + " ")) + } + }.toString() + } + } + + fun transitiveClosureGraph(roots: List, childrenFor: (T) -> List) : List> + = roots.map { transitiveClosureGraph(it, childrenFor) } + + fun transitiveClosureGraph(root: T, childrenFor: (T) -> List, seen: HashSet = hashSetOf()) : Node { + val children = arrayListOf>() + childrenFor(root).forEach { child -> + if (! seen.contains(child)) { + val c = transitiveClosureGraph(child, childrenFor) + children.add(c) + seen.add(child) + } + } + return Node(root, children) + } } + fun childrenOf(v: T) : Collection = dependedUpon[PrivateNode(v)].map { it.value } + fun transitiveClosure(root: T) - = transitiveClosure(root) { element -> dependedUpon[Node(element)].map { it.value } } + = transitiveClosure(root) { element -> dependedUpon[PrivateNode(element)].map { it.value } } fun addNode(t: T) = synchronized(nodes) { - nodes.add(Node(t)) + nodes.add(PrivateNode(t)) } fun removeNode(t: T) = synchronized(nodes) { log(VERBOSE, " Removing node $t") - Node(t).let { node -> + PrivateNode(t).let { node -> nodes.remove(node) dependingOn.removeAll(node) val set = dependedUpon.keySet() - val toReplace = arrayListOf, Collection>>>() + val toReplace = arrayListOf, Collection>>>() set.forEach { du -> val l = ArrayList(dependedUpon[du]) l.remove(node) @@ -82,10 +110,10 @@ class DynamicGraph { * Make "from" depend on "to" ("from" is no longer free). */ fun addEdge(from: T, to: T) { - val fromNode = Node(from) + val fromNode = PrivateNode(from) nodes.add(fromNode) - val toNode = Node(to) - nodes.add(Node(to)) + val toNode = PrivateNode(to) + nodes.add(PrivateNode(to)) dependingOn.put(toNode, fromNode) dependedUpon.put(fromNode, toNode) } @@ -109,7 +137,7 @@ class DynamicGraph { fun dump() : String { val result = StringBuffer() result.append("************ Graph dump ***************\n") - val free = arrayListOf>() + val free = arrayListOf>() nodes.forEach { node -> val d = dependedUpon.get(node) if (d == null || d.isEmpty()) { diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt b/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt index ce51fe51..186f54b4 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt @@ -2,7 +2,9 @@ package com.beust.kobalt.app.remote import com.beust.kobalt.Args import com.beust.kobalt.api.IClasspathDependency +import com.beust.kobalt.api.Project import com.beust.kobalt.app.BuildFileCompiler +import com.beust.kobalt.internal.DynamicGraph import com.beust.kobalt.internal.PluginInfo import com.beust.kobalt.internal.TaskManager import com.beust.kobalt.internal.build.BuildFile @@ -24,8 +26,9 @@ interface IProgressListener { class DependencyData @Inject constructor(val executors: KobaltExecutors, val dependencyManager: DependencyManager, val buildFileCompilerFactory: BuildFileCompiler.IFactory, val pluginInfo: PluginInfo, val taskManager: TaskManager) { - fun dependenciesDataFor(buildFilePath: String, args: Args, progressListener: IProgressListener? = null) - : GetDependenciesData { + + fun dependenciesDataFor(buildFilePath: String, args: Args, progressListener: IProgressListener? = null, + useGraph : Boolean = false): GetDependenciesData { val projectDatas = arrayListOf() fun toDependencyData(d: IClasspathDependency, scope: String): DependencyData { @@ -44,16 +47,48 @@ class DependencyData @Inject constructor(val executors: KobaltExecutors, val dep FileDependency(it.absolutePath) } + fun compileDependencies(project: Project, name: String): List { + val result = + (pluginDependencies + + allDeps(project.compileDependencies, name) + + allDeps(project.compileProvidedDependencies, name)) + .map { toDependencyData(it, "compile") } + return result + } + + fun toDependencyData2(scope: String, node: DynamicGraph.Companion.Node): DependencyData { + val d = node.value + val dep = dependencyManager.create(d.id) + return DependencyData(d.id, scope, dep.jarFile.get().absolutePath, + node.children.map { toDependencyData2(scope, it) }) + } + + fun compileDependenciesGraph(project: Project, name: String): List { + val depLambda = { dep : IClasspathDependency -> dep.directDependencies() } + val result = + (DynamicGraph.Companion.transitiveClosureGraph(pluginDependencies, depLambda) + + DynamicGraph.Companion.transitiveClosureGraph(project.compileDependencies, depLambda) + + DynamicGraph.Companion.transitiveClosureGraph(project.compileProvidedDependencies, depLambda)) + .map { toDependencyData2("compile", it)} + return result + } + + fun testDependencies(project: Project, name: String): List { + return allDeps(project.testDependencies, name).map { toDependencyData(it, "testCompile") } + } + + fun testDependenciesGraph(project: Project, name: String): List { + val depLambda = { dep : IClasspathDependency -> dep.directDependencies() } + return DynamicGraph.Companion.transitiveClosureGraph(project.testDependencies, depLambda) + .map { toDependencyData2("compile", it)} + } + val allTasks = hashSetOf() projectResult.projects.withIndex().forEach { wi -> val project = wi.value val name = project.name progressListener?.onProgress(message = "Synchronizing project ${project.name} " + (wi.index + 1) + "/" + projectResult.projects.size) - val compileDependencies = pluginDependencies.map { toDependencyData(it, "compile") } + - allDeps(project.compileDependencies, name).map { toDependencyData(it, "compile") } + - allDeps(project.compileProvidedDependencies, name).map { toDependencyData(it, "compile") } - val testDependencies = allDeps(project.testDependencies, name).map { toDependencyData(it, "testCompile") } val dependentProjects = project.dependsOn.map { it.name } @@ -64,6 +99,13 @@ class DependencyData @Inject constructor(val executors: KobaltExecutors, val dep TaskData(it.name, it.doc, it.group) } allTasks.addAll(projectTasks) + val compileDependencies = + if (useGraph) compileDependenciesGraph(project, project.name) + else compileDependencies(project, project.name) + val testDependencies = + if (useGraph) testDependenciesGraph(project, project.name) + else testDependencies(project, project.name) + projectDatas.add(ProjectData(project.name, project.directory, dependentProjects, compileDependencies, testDependencies, sources.second.toSet(), tests.second.toSet(), sources.first.toSet(), tests.first.toSet(), @@ -77,7 +119,8 @@ class DependencyData @Inject constructor(val executors: KobaltExecutors, val dep // use these same classes. // - class DependencyData(val id: String, val scope: String, val path: String) + class DependencyData(val id: String, val scope: String, val path: String, + val children: List = emptyList()) data class TaskData(val name: String, val description: String, val group: String) { override fun toString() = name } diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/GetDependenciesCommand.kt b/src/main/kotlin/com/beust/kobalt/app/remote/GetDependenciesCommand.kt index 21d5f59f..a46e019a 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/GetDependenciesCommand.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/GetDependenciesCommand.kt @@ -14,6 +14,7 @@ import javax.inject.Inject * { "name" : "getDependencies", "buildFile": "/Users/beust/kotlin/kobalt/kobalt/src/Build.kt" } * The response is a GetDependenciesData. */ +@Deprecated(message = "Only used by old server, to be deleted") class GetDependenciesCommand @Inject constructor(val args: Args, val dependencyData: DependencyData) : ICommand { override val name = "getDependencies" diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/GetDependencyGraphHandler.kt b/src/main/kotlin/com/beust/kobalt/app/remote/GetDependencyGraphHandler.kt new file mode 100644 index 00000000..9b066a1e --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/app/remote/GetDependencyGraphHandler.kt @@ -0,0 +1,100 @@ +package com.beust.kobalt.app.remote + +import com.beust.kobalt.Args +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.app.ProjectFinder +import com.beust.kobalt.internal.build.BuildFile +import com.beust.kobalt.internal.eventbus.ArtifactDownloadedEvent +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import com.google.gson.Gson +import org.eclipse.jetty.websocket.api.RemoteEndpoint +import org.eclipse.jetty.websocket.api.Session +import org.eclipse.jetty.websocket.api.WebSocketListener +import java.nio.file.Paths + +/** + * Manage the websocket endpoint "/v1/getDependencyGraph". + */ +class GetDependencyGraphHandler : WebSocketListener { + // The SparkJava project refused to merge https://github.com/perwendel/spark/pull/383 + // so I have to do dependency injections manually :-( + val projectFinder = Kobalt.INJECTOR.getInstance(ProjectFinder::class.java) + + var session: Session? = null + + override fun onWebSocketClose(code: Int, reason: String?) { + println("ON CLOSE $code reason: $reason") + } + + override fun onWebSocketError(cause: Throwable?) { + cause?.printStackTrace() + throw UnsupportedOperationException() + } + + fun sendWebsocketCommand(endpoint: RemoteEndpoint, commandName: String, payload: T) { + endpoint.sendString(Gson().toJson(WebSocketCommand(commandName, payload = Gson().toJson(payload)))) + } + + override fun onWebSocketConnect(s: Session) { + session = s + val buildFileParams = s.upgradeRequest.parameterMap["buildFile"] + if (buildFileParams != null) { + val buildFile = buildFileParams[0] + + fun getInstance(cls: Class) : T = Kobalt.INJECTOR.getInstance(cls) + + val result = if (buildFile != null) { + // Track all the downloads that this dependency call might trigger and + // send them as a progress message to the web socket + val eventBus = getInstance(EventBus::class.java) + val busListener = object { + @Subscribe + fun onArtifactDownloaded(event: ArtifactDownloadedEvent) { + sendWebsocketCommand(s.remote, ProgressCommand.NAME, + ProgressCommand(null, "Downloaded " + event.artifactId)) + } + } + eventBus.register(busListener) + + // Get the dependencies for the requested build file and send progress to the web + // socket for each project + try { + val dependencyData = getInstance(DependencyData::class.java) + val args = getInstance(Args::class.java) + + val allProjects = projectFinder.initForBuildFile(BuildFile(Paths.get(buildFile), buildFile), + args) + + dependencyData.dependenciesDataFor(buildFile, args, object : IProgressListener { + override fun onProgress(progress: Int?, message: String?) { + sendWebsocketCommand(s.remote, ProgressCommand.NAME, ProgressCommand(progress, message)) + } + }, useGraph = true) + } catch(ex: Throwable) { + ex.printStackTrace() + val errorMessage = ex.stackTrace.map { it.toString() }.joinToString("\n

") + DependencyData.GetDependenciesData(errorMessage = errorMessage) + } finally { + SparkServer.cleanUpCallback() + eventBus.unregister(busListener) + } + } else { + DependencyData.GetDependenciesData( + errorMessage = "buildFile wasn't passed in the query parameter") + } + sendWebsocketCommand(s.remote, DependencyData.GetDependenciesData.NAME, result) + s.close() + } + } + + override fun onWebSocketText(message: String?) { + println("RECEIVED TEXT: $message") + session?.remote?.sendString("Response: $message") + } + + override fun onWebSocketBinary(payload: ByteArray?, offset: Int, len: Int) { + println("RECEIVED BINARY: $payload") + } + +} diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt index 8787646e..10ad2677 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt @@ -4,16 +4,16 @@ import com.beust.kobalt.Args import com.beust.kobalt.api.Kobalt import com.beust.kobalt.app.MainModule import com.beust.kobalt.internal.KobaltSettings -import com.beust.kobalt.internal.remote.ICommand import com.google.gson.Gson -enum class Command(val n: Int, val command: ICommand) { - GET_DEPENDENCIES(1, Kobalt.INJECTOR.getInstance(GetDependenciesCommand::class.java)); - companion object { - val commandMap = hashMapOf() - fun commandFor(n: Int) = values().filter { it.n == n }[0].command - } -} +//enum class Command(val n: Int, val command: ICommand) { +// GET_DEPENDENCIES(1, Kobalt.INJECTOR.getInstance(GetDependenciesCommand::class.java)), +// GET_DEPENDENCIES_GRAPH(2, Kobalt.INJECTOR.getInstance(GetDependenciesGraphCommand::class.java)); +// companion object { +// val commandMap = hashMapOf() +// fun commandFor(n: Int) = values().filter { it.n == n }[0].command +// } +//} class KobaltHub(val dependencyData: DependencyData) { val args = Args() @@ -23,6 +23,9 @@ class KobaltHub(val dependencyData: DependencyData) { when(n) { 1 -> Gson().toJson( dependencyData.dependenciesDataFor("/Users/beust/kotlin/klaxon/kobalt/src/Build.kt", args)) + 2 -> Gson().toJson( + dependencyData.dependenciesDataFor("/Users/beust/kotlin/klaxon/kobalt/src/Build.kt", args, + useGraph = true)) else -> throw RuntimeException("Unknown command") } println("Data: $data") diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/OldServer.kt b/src/main/kotlin/com/beust/kobalt/app/remote/OldServer.kt index 70f23f40..e997364a 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/OldServer.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/OldServer.kt @@ -15,6 +15,7 @@ import java.io.PrintWriter import java.net.ServerSocket import java.net.SocketException +@Deprecated(message = "Replaced by Websocket server, to be deleted") class OldServer(val initCallback: (String) -> List, val cleanUpCallback: () -> Unit) : KobaltServer.IServer, ICommandSender { val pending = arrayListOf() diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt b/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt index b6e9edd0..435e2ef7 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt @@ -49,6 +49,7 @@ class SparkServer(val initCallback: (String) -> List, val cleanUpCallba log.debug("RUNNING") Spark.port(port) Spark.webSocket("/v1/getDependencies", GetDependenciesHandler::class.java) + Spark.webSocket("/v1/getDependencyGraph", GetDependencyGraphHandler::class.java) Spark.get("/ping", { req, res -> """ { "result" : "ok" } """ }) Spark.get("/quit", { req, res -> Executors.newFixedThreadPool(1).let { executor -> @@ -96,6 +97,7 @@ class SparkServer(val initCallback: (String) -> List, val cleanUpCallba /** * Manage the websocket endpoint "/v1/getDependencies". */ +@Deprecated(message = "Replaced with GetDependencyGraphHandler") class GetDependenciesHandler : WebSocketListener { // The SparkJava project refused to merge https://github.com/perwendel/spark/pull/383 // so I have to do dependency injections manually :-( diff --git a/src/test/kotlin/com/beust/kobalt/internal/DynamicGraphTest.kt b/src/test/kotlin/com/beust/kobalt/internal/DynamicGraphTest.kt index 853cc4d0..4ad44cff 100644 --- a/src/test/kotlin/com/beust/kobalt/internal/DynamicGraphTest.kt +++ b/src/test/kotlin/com/beust/kobalt/internal/DynamicGraphTest.kt @@ -185,4 +185,23 @@ class DynamicGraphTest { } } + @Test + fun transitiveClosureGraphTest() { + val graph = DynamicGraph().apply { + // a -> b + // b -> c, d + // e + addEdge("a", "b") + addEdge("b", "c") + addEdge("b", "d") + addNode("e") + } + val closure = DynamicGraph.transitiveClosureGraph("a", { s -> graph.childrenOf(s).toList() } ) + assertThat(closure.value).isEqualTo("a") + val ca = closure.children + assertThat(ca.map { it.value }).isEqualTo(listOf("b")) + val cb = ca[0].children + assertThat(cb.map { it.value }).isEqualTo(listOf("d", "c")) + + } } \ No newline at end of file