Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Implement Coursier for plugin dependency resolution and fetching #113

Merged
merged 7 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add coursier to resolve and fetch dependencies for plugins
  • Loading branch information
hlxid committed Aug 8, 2019
commit e88e58754bf5bd5d8fc702883d90bd1c0fed2b5c
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ libraryDependencies += "com.fazecast" % "jSerialComm" % "[2.0.0,3.0.0)"
// Socket.io
libraryDependencies += "io.socket" % "socket.io-client" % "1.0.0"

// Coursier
libraryDependencies += "io.get-coursier" %% "coursier" % "2.0.0-RC3-2"

// ---------------------------------------------------------------------------------------------------------------------
// PLUGIN FRAMEWORK DEFINITIONS
// ---------------------------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
package org.codeoverflow.chatoverflow.framework

import java.io.File

import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.api.APIVersion
import org.codeoverflow.chatoverflow.api.plugin.{Plugin, PluginManager}
import org.codeoverflow.chatoverflow.framework.PluginCompatibilityState.PluginCompatibilityState

import scala.concurrent.Future

/**
* A plugin type is a container for all information about a plugin, everything in the 'plugin.xml' and the actual class.
* The plugins functionality and meta information can be accessed through this interface.
*
* @param name the name of the plugin, used for identifying
* @param author the author of the plugin, used for identifying
* @param version the version of the plugin
* @param majorAPIVersion the major api version, with which the plugin was developed
* @param minorAPIVersion the minor api version, with which the plugin was developed
* @param pluginClass the class of the plugin, used to create instances of this plugin.
* Needs to have a constructor with the signature of one PluginManager,
* otherwise instances can't be created from it.
*/
* A plugin type is a container for all information about a plugin, everything in the 'plugin.xml' and the actual class.
* The plugins functionality and meta information can be accessed through this interface.
*
* @param name the name of the plugin, used for identifying
* @param author the author of the plugin, used for identifying
* @param version the version of the plugin
* @param majorAPIVersion the major api version, with which the plugin was developed
* @param minorAPIVersion the minor api version, with which the plugin was developed
* @param pluginClass the class of the plugin, used to create instances of this plugin.
* Needs to have a constructor with the signature of one PluginManager,
* otherwise instances can't be created from it.
* @param pluginDependencies A future that completes when all dependencies, that the plugin has, are available and
* that returns a seq of the required dependencies files in the local coursier cache.
*/
class PluginType(name: String, author: String, version: String, majorAPIVersion: Int, minorAPIVersion: Int,
metadata: PluginMetadata, pluginClass: Class[_ <: Plugin]) extends WithLogger {
metadata: PluginMetadata, pluginClass: Class[_ <: Plugin], pluginDependencies: Future[Seq[File]]) extends WithLogger {

private var pluginVersionState = PluginCompatibilityState.Untested

Expand Down Expand Up @@ -126,4 +132,12 @@ class PluginType(name: String, author: String, version: String, majorAPIVersion:
* @return the PluginMetadata instance of this plugin
*/
def getMetadata: PluginMetadata = metadata

/**
* Returns the future that will result in a sequence of jar files that represents the dependencies
* of this plugin including all sub-dependencies.
*
* @return the dependency future
*/
def getDependencyFuture: Future[Seq[File]] = pluginDependencies
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.codeoverflow.chatoverflow.framework.helper

import java.io.{File, InputStream}

import coursier.Fetch
import coursier.cache.{CacheLogger, FileCache}
import coursier.core.Dependency
import coursier.maven.PomParser
import org.codeoverflow.chatoverflow.WithLogger

import scala.io.Source

/**
* A utility object containing some common code for use with Coursier.
*/
object CoursierUtils extends WithLogger {

private object CoursierLogger extends CacheLogger {
override def downloadedArtifact(url: String, success: Boolean): Unit = {
logger debug (if (success)
s"Successfully downloaded $url"
else
s"Failed to download $url")
}
}

private val cache = FileCache().noCredentials.withLogger(CoursierLogger)

/**
* Extracts all dependencies out of the provided pom. Throws an exception if the pom is invalid.
*
* @param is the InputStream from which the pom is read
* @return a seq of all found dependencies
*/
def parsePom(is: InputStream): Seq[Dependency] = {
val pomFile = Source.fromInputStream(is)
val parser = coursier.core.compatibility.xmlParseSax(pomFile.mkString, new PomParser)

parser.project match {
case Right(deps) => deps.dependencies.map(_._2)
case Left(errorMsg) => throw new IllegalArgumentException(s"Pom couldn't be parsed: $errorMsg")
}
}

/**
* Resolves and fetches all passed dependencies and gives back a seq of all local files of these dependencies.
*
* @param dependencies all dependencies that you want to be fetched
* @return all local files for the passed dependencies
*/
def fetchDependencies(dependencies: Seq[Dependency]): Seq[File] = {
// IntelliJ may warn you that a implicit is missing. This is one of the many bugs in IntelliJ, the code compiles fine.
Fetch()
.withCache(cache)
.addDependencies(dependencies: _*)
.run()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package org.codeoverflow.chatoverflow.framework.helper
import java.net.{URL, URLClassLoader}

/**
* This plugin class loader does only exist for plugin security policy checks.
*
* @param urls Takes an array of urls an creates a simple URLClassLoader with it
*/
class PluginClassLoader(urls: Array[URL]) extends URLClassLoader(urls)
* This plugin class loader does only exist for plugin security policy checks and
* to expose the addURL method to the package inorder to add all required dependencies after dependency resolution.
*
* @param urls Takes an array of urls an creates a simple URLClassLoader with it
*/
class PluginClassLoader(urls: Array[URL]) extends URLClassLoader(urls) {
private[helper] override def addURL(url: URL): Unit = super.addURL(url) // just exposes this method to be package-private instead of protected
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import org.reflections.scanners.SubTypesScanner
import org.reflections.util.ConfigurationBuilder

import scala.collection.JavaConverters._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Success}
import scala.xml.{Node, SAXParseException, XML}

/**
Expand Down Expand Up @@ -80,7 +83,8 @@ class PluginLoader(private val jar: File) extends WithLogger {
majorString.toInt,
minorString.toInt,
PluginMetadata.fromXML(p),
cls
cls,
resolveDependencies(getString(p, "name"))
))
} catch {
// thrown by getString
Expand Down Expand Up @@ -144,4 +148,29 @@ class PluginLoader(private val jar: File) extends WithLogger {
None
}
}

/**
* Creates a future which gets all dependencies from the included dependencies.pom, if existing, fetches them
* and adds their jar files to the classloader.
*
* @param pluginName the name of the plugin, only used for logging
* @return a future of all required jars for this plugin
*/
private def resolveDependencies(pluginName: String): Future[Seq[File]] = {
val pomIs = classloader.getResourceAsStream("dependencies.pom")
if (pomIs == null) {
return Future(Seq())
}

Future(CoursierUtils.parsePom(pomIs))
.map(dependencies => dependencies.filter(_.module.name.value != "chatoverflow-api_2.12"))
.map(dependencies => CoursierUtils.fetchDependencies(dependencies))
.andThen {
case Success(jarFiles) =>
jarFiles.foreach(jar => classloader.addURL(jar.toURI.toURL))
logger info s"Dependencies for the plugin $pluginName successfully resolved and fetched if missing."
case Failure(exception) =>
logger warn s"Couldn't resolve and fetch dependencies for the plugin in $pluginName: $exception"
}
}
}