mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-07-03 05:47:24 +00:00
feat(extension): 支持从 Maven 仓库下载并安装扩展包.
支持该功能后, 可以使用户更容易安装扩展包, 尤其是无需配置的扩展包.
This commit is contained in:
@ -1,15 +1,33 @@
|
||||
package net.lamgc.scalabot
|
||||
|
||||
import com.google.common.base.Throwables
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.util.deepListFiles
|
||||
import net.lamgc.scalabot.util.equalsArtifact
|
||||
import org.apache.maven.repository.internal.MavenRepositorySystemUtils
|
||||
import org.eclipse.aether.RepositorySystem
|
||||
import org.eclipse.aether.artifact.Artifact
|
||||
import org.eclipse.aether.artifact.DefaultArtifact
|
||||
import org.eclipse.aether.collection.CollectRequest
|
||||
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
|
||||
import org.eclipse.aether.graph.Dependency
|
||||
import org.eclipse.aether.repository.*
|
||||
import org.eclipse.aether.resolution.ArtifactRequest
|
||||
import org.eclipse.aether.resolution.ArtifactResult
|
||||
import org.eclipse.aether.resolution.DependencyRequest
|
||||
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
|
||||
import org.eclipse.aether.spi.connector.transport.TransporterFactory
|
||||
import org.eclipse.aether.transport.file.FileTransporterFactory
|
||||
import org.eclipse.aether.transport.http.HttpTransporterFactory
|
||||
import org.eclipse.aether.util.filter.ScopeDependencyFilter
|
||||
import org.jdom2.Document
|
||||
import org.jdom2.filter.Filters
|
||||
import org.jdom2.input.SAXBuilder
|
||||
import org.jdom2.xpath.XPathFactory
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.util.*
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarInputStream
|
||||
@ -20,14 +38,22 @@ import java.util.jar.JarInputStream
|
||||
*
|
||||
* 将搜索文件名(不带扩展包名)结尾为 `${groupId}_${artifactId}_${version}` 的文件.
|
||||
* 比如说 `(Example Extension) org.example_scalabot-example_v1.0.0-SNAPSHOT.jar` 是可以的
|
||||
*
|
||||
* 使用这种方式, 要求扩展包将依赖打包进自己的 jar, 可能会出现分发包体积较大的情况.
|
||||
*/
|
||||
@FinderRules(priority = FinderPriority.LOCAL)
|
||||
internal object FileNameFinder : ExtensionPackageFinder {
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
private val allowExtensionNames = setOf("jar", "jmod", "zip")
|
||||
|
||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||
val focusName = getExtensionFilename(extensionArtifact)
|
||||
log.debug { "扩展 $extensionArtifact 匹配规则: $focusName" }
|
||||
val files = extensionsPath.listFiles { file ->
|
||||
file.nameWithoutExtension.endsWith(focusName)
|
||||
file.nameWithoutExtension.endsWith(focusName) &&
|
||||
(allowExtensionNames.contains(file.extension.lowercase()) || file.isDirectory)
|
||||
} ?: return emptySet()
|
||||
|
||||
val extensionPackage = mutableSetOf<FoundExtensionPackage>()
|
||||
@ -195,3 +221,247 @@ internal object MavenMetaInformationFinder : ExtensionPackageFinder {
|
||||
!prop.containsKey(key) || prop.getProperty(key).trim().isEmpty()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定的 Maven 仓库下载扩展包.
|
||||
*
|
||||
* 注: 也会下载扩展包的依赖包.
|
||||
*
|
||||
* 建议扩展包不要将依赖项打包到本体, 搜索器会自动下载依赖项并自动缓存, 这对于后续更新来讲十分有用. (也会给其他扩展包共享依赖)
|
||||
* 目前每个扩展包的依赖都是分开加载的, 我会听取社区意见, 来决定是否有必要让依赖项共享同一个 [ClassLoader].
|
||||
*
|
||||
* 推荐使用这种方式来安装扩展.
|
||||
*/
|
||||
@FinderRules(priority = FinderPriority.REMOTE)
|
||||
internal class MavenRepositoryExtensionFinder(
|
||||
private val localRepository: LocalRepository,
|
||||
private val proxy: Proxy? = null,
|
||||
private val remoteRepositories: List<RemoteRepository> = listOf(getMavenCentralRepository(proxy)),
|
||||
) : ExtensionPackageFinder {
|
||||
|
||||
private val repositorySystem = createRepositorySystem()
|
||||
|
||||
private val repoSystemSession = createRepositorySystemSession()
|
||||
|
||||
private fun createRepositorySystem() = MavenRepositorySystemUtils.newServiceLocator().apply {
|
||||
addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java)
|
||||
addService(TransporterFactory::class.java, FileTransporterFactory::class.java)
|
||||
addService(TransporterFactory::class.java, HttpTransporterFactory::class.java)
|
||||
}.getService(RepositorySystem::class.java)
|
||||
|
||||
private fun createRepositorySystemSession() = MavenRepositorySystemUtils.newSession().apply {
|
||||
localRepositoryManager = repositorySystem.newLocalRepositoryManager(
|
||||
this,
|
||||
this@MavenRepositoryExtensionFinder.localRepository
|
||||
)
|
||||
}
|
||||
|
||||
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
|
||||
val extensionArtifactResult = repositorySystem.resolveArtifact(
|
||||
repoSystemSession,
|
||||
ArtifactRequest(extensionArtifact, remoteRepositories, null)
|
||||
)
|
||||
val extResolvedArtifact = extensionArtifactResult.artifact
|
||||
if (!extensionArtifactResult.isResolved) {
|
||||
if (extensionArtifactResult.isMissing) {
|
||||
log.warn { "在指定的仓库中找不到构件: ${extensionArtifactResult.artifact}" }
|
||||
} else {
|
||||
printArtifactResultExceptions(extensionArtifactResult.exceptions)
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
val request = DependencyRequest(
|
||||
CollectRequest(Dependency(extResolvedArtifact, null), remoteRepositories),
|
||||
ScopeDependencyFilter(setOf("runtime", "compile", "provided"), null)
|
||||
)
|
||||
val dependencyResult = repositorySystem.resolveDependencies(repoSystemSession, request)
|
||||
val dependencies = checkAndCollectDependencyArtifacts(extensionArtifact, dependencyResult.artifactResults)
|
||||
?: return emptySet()
|
||||
|
||||
return setOf(MavenExtensionPackage(this, extResolvedArtifact, extensionArtifactResult.repository, dependencies))
|
||||
}
|
||||
|
||||
private fun checkAndCollectDependencyArtifacts(
|
||||
extensionArtifact: Artifact,
|
||||
dependencyResult: List<ArtifactResult>
|
||||
): Set<Artifact>? {
|
||||
val resolveFailedArtifacts = mutableSetOf<ArtifactResult>()
|
||||
for (artifactResult in dependencyResult) {
|
||||
if (!artifactResult.isResolved) {
|
||||
resolveFailedArtifacts.add(artifactResult)
|
||||
}
|
||||
}
|
||||
|
||||
if (resolveFailedArtifacts.isNotEmpty()) {
|
||||
log.error {
|
||||
StringBuilder("扩展包 `$extensionArtifact` 下列依赖项获取失败: \n").apply {
|
||||
resolveFailedArtifacts.forEach {
|
||||
append("\t- 依赖项 `${it.artifact}` ")
|
||||
if (it.isMissing) {
|
||||
append(" 无法从指定仓库中找到.")
|
||||
} else {
|
||||
append("\n")
|
||||
for (e in it.exceptions) {
|
||||
append("\t\t- ${e::class.java.name}: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return dependencyResult.map {
|
||||
log.debug { "依赖项 ${it.artifact} 文件路径: ${it.artifact.file}" }
|
||||
it.artifact!!
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
private fun printArtifactResultExceptions(exceptions: List<Exception>) {
|
||||
log.warn {
|
||||
val builder = StringBuilder("构件可能已找到, 但由于以下原因导致获取失败: \n")
|
||||
exceptions.forEachIndexed { index, exception ->
|
||||
builder.append("[$index] ").append(Throwables.getStackTraceAsString(exception)).append('\n')
|
||||
}
|
||||
return@warn builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private val log = KotlinLogging.logger { }
|
||||
|
||||
/**
|
||||
* 将 [URL] 转换成 [RemoteRepository] 对象.
|
||||
* @param url 远端仓库地址.
|
||||
* @param type 仓库布局类型, 如果是 Maven 2 的仓库布局, 则为 `default`, 如果是 Maven 1 的旧版仓库布局, 则为 `legacy`.
|
||||
* @param proxy 是否使用代理访问仓库, 默认为 `null`.
|
||||
*/
|
||||
fun resolveRepositoryByUrl(
|
||||
url: URL,
|
||||
repoId: String? = null,
|
||||
type: String = "default",
|
||||
proxy: Proxy?,
|
||||
authentication: Authentication? = null
|
||||
): RemoteRepository {
|
||||
val builder = RemoteRepository.Builder(repoId, type, url.toString())
|
||||
if (proxy != null) {
|
||||
builder.setProxy(proxy)
|
||||
}
|
||||
builder.setReleasePolicy(
|
||||
RepositoryPolicy(
|
||||
true,
|
||||
RepositoryPolicy.UPDATE_POLICY_DAILY,
|
||||
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
||||
)
|
||||
)
|
||||
builder.setSnapshotPolicy(
|
||||
RepositoryPolicy(
|
||||
true,
|
||||
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
||||
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
||||
)
|
||||
)
|
||||
if (authentication != null) {
|
||||
builder.setAuthentication(authentication)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Maven 中央仓库 Url.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
const val MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/"
|
||||
|
||||
/**
|
||||
* 获取 Maven 中央仓库的 [RemoteRepository] 对象.
|
||||
*/
|
||||
fun getMavenCentralRepository(proxy: Proxy? = null): RemoteRepository {
|
||||
val builder = RemoteRepository.Builder("central", "default", MAVEN_CENTRAL_URL)
|
||||
if (proxy != null) {
|
||||
builder.setProxy(proxy)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MavenExtensionPackage(
|
||||
private val finder: ExtensionPackageFinder,
|
||||
private val artifact: Artifact,
|
||||
private val fromRepository: ArtifactRepository,
|
||||
val dependencies: Set<Artifact>
|
||||
) : FoundExtensionPackage {
|
||||
override fun getExtensionArtifact(): Artifact = artifact
|
||||
|
||||
override fun getRawUrl(): URL {
|
||||
return if (fromRepository is RemoteRepository) {
|
||||
val urlStr = if (fromRepository.url.endsWith("/")) {
|
||||
fromRepository.url + getArtifactPath(artifact)
|
||||
} else {
|
||||
fromRepository.url + "/" + getArtifactPath(artifact)
|
||||
}
|
||||
URL(urlStr)
|
||||
} else {
|
||||
getPackageFile().toURI().toURL()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPackageFile(): File = artifact.file
|
||||
|
||||
override fun getExtensionPackageFinder() = finder
|
||||
|
||||
/**
|
||||
* 获取 Artifact 在 Maven 仓库中的路径.
|
||||
*
|
||||
* 遵循 [Repository Layout - Final](https://cwiki.apache.org/confluence/display/MAVENOLD/Repository+Layout+-+Final)
|
||||
*/
|
||||
private fun getArtifactPath(artifact: Artifact): String {
|
||||
val groups = artifact.groupId.split('.')
|
||||
return StringBuilder("/").apply {
|
||||
for (group in groups) {
|
||||
append(group).append('/')
|
||||
}
|
||||
append("${artifact.artifactId}/${artifact.version}/")
|
||||
append("${artifact.artifactId}-${artifact.version}")
|
||||
if (artifact.classifier.isNotEmpty()) {
|
||||
append("-${artifact.classifier}")
|
||||
}
|
||||
append(".${artifact.extension}")
|
||||
}.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getClassLoaderFactory(): ExtensionClassLoaderFactory {
|
||||
return object : ExtensionClassLoaderFactory {
|
||||
override fun createClassLoader(foundExtensionPackage: FoundExtensionPackage): ExtensionClassLoader {
|
||||
if (foundExtensionPackage !is MavenExtensionPackage) {
|
||||
throw IllegalArgumentException("Unsupported FoundExtensionPackage type: $foundExtensionPackage")
|
||||
}
|
||||
|
||||
val urls = mutableSetOf<URL>()
|
||||
for (dependency in foundExtensionPackage.dependencies) {
|
||||
val dependencyFile = dependency.file ?: continue
|
||||
urls.add(dependencyFile.toURI().toURL())
|
||||
}
|
||||
|
||||
// 将依赖的 ClassLoader 与 ExtensionPackage 的 ClassLoader 分开
|
||||
// 这么做可以防范依赖中隐藏的 SPI 注册, 避免安全隐患.
|
||||
|
||||
val dependenciesUrlArray = urls.toTypedArray()
|
||||
val dependenciesClassLoader = URLClassLoader(dependenciesUrlArray)
|
||||
|
||||
return ExtensionClassLoader(
|
||||
arrayOf(foundExtensionPackage.getPackageFile().toURI().toURL()),
|
||||
dependenciesClassLoader
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user