1
0
Fork 0
mirror of https://github.com/ethauvin/kobalt.git synced 2025-04-26 08:27:12 -07:00

Merge pull request #79 from evanchooly/master

fixes #12 -- adds support for version ranges
This commit is contained in:
Cedric Beust 2015-12-16 06:25:45 +04:00
commit 9be5dba56e
10 changed files with 489 additions and 37 deletions

View file

@ -35,15 +35,15 @@ public class DepFactory @Inject constructor(val localRepo: LocalRepo,
var packaging = mavenId.packaging
var repoResult: RepoFinder.RepoResult?
if (! mavenId.hasVersion) {
if (localFirst) version = localRepo.findLocalVersion(mavenId.groupId, mavenId.artifactId,
mavenId.packaging)
if (! localFirst || version == null) {
if (mavenId.version != null) {
var localVersion: String? = mavenId.version
if (localFirst) localVersion = localRepo.findLocalVersion(mavenId.groupId, mavenId.artifactId, mavenId.packaging)
if (! localFirst || localVersion == null) {
repoResult = repoFinder.findCorrectRepo(id)
if (!repoResult.found) {
throw KobaltException("Couldn't resolve $id")
} else {
version = repoResult.version
version = repoResult.version?.version
}
}
}

View file

@ -20,7 +20,13 @@ class MavenId private constructor(val groupId: String, val artifactId: String, v
size == 3 || size == 4
}
private fun isVersion(s: String) : Boolean = Character.isDigit(s[0])
private fun isVersion(s: String): Boolean {
return Character.isDigit(s[0]) || isRangedVersion(s)
}
fun isRangedVersion(s: String): Boolean {
return s.first() in listOf('[', '(') && s.last() in listOf(']', ')')
}
/**
* Similar to create(MavenId) but don't run IMavenIdInterceptors.
@ -38,12 +44,11 @@ class MavenId private constructor(val groupId: String, val artifactId: String, v
groupId = c[0]
artifactId = c[1]
if (!c[2].isEmpty()) {
if (isVersion(c[2])) {
version = c[2]
} else {
packaging = c[2]
version = c[3]
val split = c[2].split('@')
if (isVersion(split[0])) {
version = split[0]
}
packaging = if (split.size == 2) split[1] else null
}
return MavenId(groupId, artifactId, packaging, version)

View file

@ -7,7 +7,9 @@ import com.beust.kobalt.misc.*
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
import kotlinx.dom.asElementList
import kotlinx.dom.parseXml
import org.w3c.dom.NodeList
import java.io.File
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorCompletionService
@ -25,8 +27,8 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
return FOUND_REPOS.get(id)
}
data class RepoResult(val hostConfig: HostConfig, val found: Boolean, val version: String,
val hasJar: Boolean = true, val snapshotVersion: String = "")
data class RepoResult(val hostConfig: HostConfig, val found: Boolean, val version: Version? = null,
val hasJar: Boolean = true, val snapshotVersion: Version? = null)
private val FOUND_REPOS: LoadingCache<String, RepoResult> = CacheBuilder.newBuilder()
.build(object : CacheLoader<String, RepoResult>() {
@ -62,10 +64,11 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
}
if (results.size > 0) {
results.sortByDescending { Versions.toLongVersion(it.version) }
// results.sortByDescending { Versions.toLongVersion(it.version) }
results.sort({ left, right -> left.version!!.compareTo(right.version!!) })
return results[0]
} else {
return RepoResult(HostConfig(""), false, id)
return RepoResult(HostConfig(""), false, Version.of(id))
}
}
@ -81,28 +84,36 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
val groupId = mavenId.groupId
val artifactId = mavenId.artifactId
if (! mavenId.hasVersion) {
if (mavenId.version == null) {
val ud = UnversionedDep(groupId, artifactId)
val isLocal = repoUrl.startsWith(FileDependency.PREFIX_FILE)
val foundVersion = findCorrectVersionRelease(ud.toMetadataXmlPath(false, isLocal), repoUrl)
if (foundVersion != null) {
return RepoResult(repo, true, foundVersion)
return RepoResult(repo, true, Version.of(foundVersion))
} else {
return RepoResult(repo, false, "")
return RepoResult(repo, false)
}
} else {
val version = mavenId.version
if (version!!.contains("SNAPSHOT")) {
val version = Version.of(mavenId.version)
if (version.isSnapshot()) {
val dep = SimpleDep(mavenId)
val isLocal = repoUrl.startsWith(FileDependency.PREFIX_FILE)
val snapshotVersion = if (isLocal) version!!
else findSnapshotVersion(dep.toMetadataXmlPath(false, isLocal, version), repoUrl)
val snapshotVersion = if (isLocal) version
else findSnapshotVersion(dep.toMetadataXmlPath(false, isLocal, version.version), repoUrl)
if (snapshotVersion != null) {
return RepoResult(repo, true, version, true /* hasJar, potential bug here */,
snapshotVersion)
} else {
return RepoResult(repo, false, "")
return RepoResult(repo, false)
}
} else if (version.isRangedVersion() ) {
val foundVersion = findRangedVersion(SimpleDep(mavenId), repoUrl)
if (foundVersion != null) {
return RepoResult(repo, true, foundVersion)
} else {
return RepoResult(repo, false)
}
} else {
val dep = SimpleDep(mavenId)
// Try to find the jar file
@ -118,7 +129,7 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
true
}
log(2, "Result for $repoUrl for $id: $found")
return RepoResult(repo, found, dep.version, hasJar)
return RepoResult(repo, found, Version.of(dep.version), hasJar)
}
}
}
@ -148,7 +159,32 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
return null
}
fun findSnapshotVersion(metadataPath: String, repoUrl: String): String? {
fun findRangedVersion(dep: SimpleDep, repoUrl: String): Version? {
val l = listOf(dep.groupId.replace(".", "/"), dep.artifactId.replace(".", "/"), "maven-metadata.xml")
var metadataPath = l.joinToString("/")
val versionsXpath = XPATH.compile("/metadata/versioning/versions/version")
// No version in this dependency, find out the most recent one by parsing maven-metadata.xml, if it exists
val url = repoUrl + metadataPath
try {
val doc = parseXml(url)
val version = Version.of(dep.version)
if(version.isRangedVersion()) {
val versions = (versionsXpath.evaluate(doc, XPathConstants.NODESET) as NodeList)
.asElementList().map { Version.of(it.textContent) }
return version.select(versions)
} else {
return Version.of(XPATH.compile("/metadata/versioning/versions/version/$version")
.evaluate(doc, XPathConstants.STRING) as String)
}
} catch(ex: Exception) {
log(2, "Couldn't find metadata at ${url}")
}
return null
}
fun findSnapshotVersion(metadataPath: String, repoUrl: String): Version? {
val timestamp = XPATH.compile("/metadata/versioning/snapshot/timestamp")
val buildNumber = XPATH.compile("/metadata/versioning/snapshot/buildNumber")
// No version in this dependency, find out the most recent one by parsing maven-metadata.xml, if it exists
@ -158,11 +194,11 @@ public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
val ts = timestamp.evaluate(doc, XPathConstants.STRING)
val bn = buildNumber.evaluate(doc, XPathConstants.STRING)
if (! Strings.isEmpty(ts.toString()) && ! Strings.isEmpty(bn.toString())) {
return ts.toString() + "-" + bn.toString()
return Version.of(ts.toString() + "-" + bn.toString())
} else {
val lastUpdated = XPATH.compile("/metadata/versioning/lastUpdated")
if (! lastUpdated.toString().isEmpty()) {
return lastUpdated.toString()
return Version.of(lastUpdated.toString())
}
}

View file

@ -20,11 +20,11 @@ open class SimpleDep(open val mavenId: MavenId) : UnversionedDep(mavenId.groupId
fun toPomFile(v: String) = toFile(v, "", ".pom")
fun toPomFile(r: RepoFinder.RepoResult) = toFile(r.version, r.snapshotVersion, ".pom")
fun toPomFile(r: RepoFinder.RepoResult) = toFile(r.version!!.version, r.snapshotVersion!!.version, ".pom")
fun toJarFile(v: String = version) = toFile(v, "", suffix)
fun toJarFile(r: RepoFinder.RepoResult) = toFile(r.version, r.snapshotVersion, suffix)
fun toJarFile(r: RepoFinder.RepoResult) = toFile(r.version!!.version, r.snapshotVersion!!.version, suffix)
fun toPomFileName() = "$artifactId-$version.pom"

View file

@ -1,6 +1,12 @@
package com.beust.kobalt.misc
import com.beust.kobalt.maven.MavenId
import com.google.common.base.CharMatcher
import java.math.BigInteger
import java.util.Arrays
import java.util.Comparator
import java.util.Locale
import java.util.TreeMap
public class Versions {
companion object {
@ -32,3 +38,357 @@ public class Versions {
}
}
}
class Version(val version: String): Comparable<Version> {
companion object {
private val comparator = VersionComparator()
fun of(string: String): Version {
return Version(string)
}
}
internal val items: List<Item>
private var hash: Int = -1
init {
items = parse(version)
}
private fun parse(version: String): List<Item> {
val items = arrayListOf<Item>()
val tokenizer = Tokenizer(version)
while (tokenizer.next()) {
items.add(tokenizer.toItem())
}
trimPadding(items)
return items
}
private fun trimPadding(items: MutableList<Item>) {
var number: Boolean? = null
var end = items.size - 1
for (i in end downTo 1) {
val item = items[i]
if (item.isNumber != number) {
end = i
number = item.isNumber
}
if (end == i && (i == items.size - 1 || items[i - 1].isNumber == item.isNumber) && item.compareTo(null) == 0) {
items.removeAt(i)
end--
}
}
}
override fun compareTo(other: Version): Int {
return comparator.compare(this, other)
}
override fun equals(other: Any?): Boolean {
return (other is Version) && comparator.compare(this, other) == 0
}
override fun hashCode(): Int {
if ( hash == -1 ) hash = Arrays.hashCode(items.toTypedArray())
return hash
}
override fun toString(): String {
return version
}
fun isSnapshot(): Boolean {
return items.firstOrNull { it.isSnapshot } != null
}
fun isRangedVersion(): Boolean {
return MavenId.isRangedVersion(version)
}
fun select(list: List<Version>): Version? {
if (!(version.first() in listOf('[', '(') && version.last() in listOf(']', ')'))) {
return this
}
var lowerExclusive = version.startsWith("(")
var upperExclusive = version.endsWith(")")
val split = version.drop(1).dropLast(1).split(",")
val lower = Version.of(split[0].substring(1))
val upper = if(split.size > 1) {
Version.of(if (split[1].isNotBlank()) split[1] else Int.MAX_VALUE.toString())
} else {
lower
}
var filtered = list.filter { comparator.compare(it, lower) >= 0 && comparator.compare(it, upper) <= 0 }
if (lowerExclusive && lower.equals(filtered.firstOrNull())) {
filtered = filtered.drop(1)
}
if (upperExclusive && upper.equals(filtered.lastOrNull())) {
filtered = filtered.dropLast(1)
}
return filtered.lastOrNull();
}
}
class VersionComparator: Comparator<Version> {
override fun compare(left: Version, right: Version): Int {
val these = left.items
val those = right.items
var number = true
var index = 0
while (true) {
if (index >= these.size && index >= those.size) {
return 0
} else if (index >= these.size) {
return -comparePadding(those, index, null)
} else if (index >= those.size) {
return comparePadding(these, index, null)
}
val thisItem = these[index]
val thatItem = those[index]
if (thisItem.isNumber != thatItem.isNumber) {
if (number == thisItem.isNumber) {
return comparePadding(these, index, number)
} else {
return -comparePadding(those, index, number)
}
} else {
val rel = thisItem.compareTo(thatItem)
if (rel != 0) {
return rel
}
number = thisItem.isNumber
}
index++
}
}
private fun comparePadding(items: List<Item>, index: Int, number: Boolean?): Int {
var rel = 0
for (i in index..items.size - 1) {
val item = items[i]
if (number != null && number !== item.isNumber) {
break
}
rel = item.compareTo(null)
if (rel != 0) {
break
}
}
return normalize(rel)
}
}
internal class Item(private val kind: Int, private val value: Any) {
// i.e. kind != string/qualifier
val isNumber: Boolean
get() = (kind and KIND_QUALIFIER) == 0
val isSnapshot: Boolean
get() = (kind and KIND_QUALIFIER) != 0 && value == Tokenizer.QUALIFIER_SNAPSHOT
operator fun compareTo(that: Item?): Int {
var rel: Int
if (that == null) {
// null in this context denotes the pad item (0 or "ga")
when (kind) {
KIND_MIN -> rel = -1
KIND_MAX, KIND_BIGINT, KIND_STRING -> rel = 1
KIND_INT, KIND_QUALIFIER -> rel = value as Int
else -> throw IllegalStateException("unknown version item kind " + kind)
}
} else {
rel = kind - that.kind
if (rel == 0) {
when (kind) {
KIND_MAX, KIND_MIN -> {
}
KIND_BIGINT -> rel = (value as BigInteger).compareTo(that.value as BigInteger)
KIND_INT, KIND_QUALIFIER -> rel = (value as Int).compareTo(that.value as Int)
KIND_STRING -> rel = (value as String).compareTo(that.value as String, ignoreCase = true)
else -> throw IllegalStateException("unknown version item kind " + kind)
}
}
}
return rel
}
override fun equals(other: Any?): Boolean {
return (other is Item) && compareTo(other as Item?) == 0
}
override fun hashCode(): Int {
return value.hashCode() + kind * 31
}
override fun toString(): String {
return value.toString()
}
companion object {
val KIND_MAX = 8
val KIND_BIGINT = 5
val KIND_INT = 4
val KIND_STRING = 3
val KIND_QUALIFIER = 2
val KIND_MIN = 0
val MAX = Item(KIND_MAX, "max")
val MIN = Item(KIND_MIN, "min")
}
}
internal class Tokenizer(version: String) {
private val version: String
private var index: Int = 0
private var token: String = ""
private var number: Boolean = false
private var terminatedByNumber: Boolean = false
init {
this.version = if (version.length > 0) version else "0"
}
operator fun next(): Boolean {
val n = version.length
if (index >= n) {
return false
}
var state = -2
var start = index
var end = n
terminatedByNumber = false
while (index < n) {
val c = version[index]
if (c == '.' || c == '-' || c == '_') {
end = index
index++
break
} else {
val digit = Character.digit(c, 10)
if (digit >= 0) {
if (state == -1) {
end = index
terminatedByNumber = true
break
}
if (state == 0) {
// normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
start++
}
state = if ((state > 0 || digit > 0)) 1 else 0
} else {
if (state >= 0) {
end = index
break
}
state = -1
}
}
index++
}
if (end - start > 0) {
token = version.substring(start, end)
number = state >= 0
} else {
token = "0"
number = true
}
return true
}
override fun toString(): String {
return token.toString()
}
fun toItem(): Item {
if (number) {
try {
if (token.length < 10) {
return Item(Item.KIND_INT, Integer.parseInt(token))
} else {
return Item(Item.KIND_BIGINT, BigInteger(token))
}
} catch (e: NumberFormatException) {
throw IllegalStateException(e)
}
} else {
if (index >= version.length) {
if ("min".equals(token, ignoreCase = true)) {
return Item.MIN
} else if ("max".equals(token, ignoreCase = true)) {
return Item.MAX
}
}
if (terminatedByNumber && token.length == 1) {
when (token[0]) {
'a', 'A' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_ALPHA)
'b', 'B' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_BETA)
'm', 'M' -> return Item(Item.KIND_QUALIFIER, QUALIFIER_MILESTONE)
}
}
val qualifier = QUALIFIERS[token]
if (qualifier != null) {
return Item(Item.KIND_QUALIFIER, qualifier)
} else {
return Item(Item.KIND_STRING, token.toLowerCase(Locale.ENGLISH))
}
}
}
companion object {
internal val QUALIFIER_ALPHA = -5
internal val QUALIFIER_BETA = -4
internal val QUALIFIER_MILESTONE = -3
internal val QUALIFIER_SNAPSHOT = -1
private val QUALIFIERS = TreeMap<String, Int>(String.CASE_INSENSITIVE_ORDER)
init {
QUALIFIERS.put("alpha", QUALIFIER_ALPHA)
QUALIFIERS.put("beta", QUALIFIER_BETA)
QUALIFIERS.put("milestone", QUALIFIER_MILESTONE)
QUALIFIERS.put("snapshot", QUALIFIER_SNAPSHOT)
QUALIFIERS.put("cr", -2)
QUALIFIERS.put("rc", -2)
QUALIFIERS.put("ga", 0)
QUALIFIERS.put("final", 0)
QUALIFIERS.put("", 0)
QUALIFIERS.put("sp", 1)
}
}
}
private fun normalize(value: Int): Int {
return when {
value == 0 -> 0
value > 0 -> 1
else -> -1
}
}

View file

@ -6,8 +6,10 @@ import org.testng.annotations.Guice
@Guice(modules = arrayOf(TestModule::class))
open class KobaltTest {
companion object {
@BeforeSuite
public fun bs() {
Kobalt.INJECTOR = com.google.inject.Guice.createInjector(TestModule())
}
}
}

View file

@ -16,6 +16,8 @@ public class DependencyTest @Inject constructor(val depFactory: DepFactory,
@DataProvider
fun dpVersions(): Array<Array<out Any>> {
return arrayOf(
arrayOf("0.1", "0.1.1"),
arrayOf("0.1", "1.4"),
arrayOf("6.9.4", "6.9.5"),
arrayOf("1.7", "1.38"),
arrayOf("1.70", "1.380"),

View file

@ -72,6 +72,21 @@ public class DownloadTest @Inject constructor(
}
}
@Test
public fun shouldDownloadRangedVersion() {
File(localRepo.toFullPath("javax/servlet/servlet-api")).deleteRecursively()
testRange("[2.5,)", "3.0-alpha-1")
}
private fun testRange(range: String, expected: String) {
val dep = depFactory.create("javax.servlet:servlet-api:${range}", executor)
val future = dep.jarFile
val file = future.get()
Assert.assertFalse(future is CompletedFuture)
Assert.assertEquals(file.getName(), "servlet-api-${expected}.jar")
Assert.assertTrue(file.exists())
}
@Test
public fun shouldFindLocalJar() {
MavenDependency.create("$idNoVersion$version")

View file

@ -15,7 +15,7 @@ class MavenIdTest {
null, null),
arrayOf("com.google.inject:guice:4.0:no_aop",
"com.google.inject", "guice", "4.0", null, "no_aop"),
arrayOf("com.android.support:appcompat-v7:aar:22.2.1",
arrayOf("com.android.support:appcompat-v7:22.2.1@aar",
"com.android.support", "appcompat-v7", "22.2.1", "aar", null)
)
}

View file

@ -0,0 +1,32 @@
package com.beust.kobalt.misc
import com.beust.kobalt.KobaltTest
import org.testng.Assert
import org.testng.annotations.Test
class VersionTest : KobaltTest() {
@Test
fun snapshot() {
val version = Version.of("1.2.0-SNAPSHOT")
Assert.assertTrue(version.isSnapshot())
}
@Test
fun rangedVersions() {
val ranged = Version.of("[2.5,)")
Assert.assertTrue(ranged.isRangedVersion())
}
@Test
fun selectVersion() {
var versions = listOf("2.4.public_draft", "2.2", "2.3", "2.4", "2.4-20040521", "2.5", "3.0-alpha-1").map { Version.of(it) }
Assert.assertEquals(Version.of("[2.5,)").select(versions), Version.of("3.0-alpha-1"))
Assert.assertEquals(Version.of("[2.5,3.0)").select(versions), Version.of("3.0-alpha-1"))
Assert.assertEquals(Version.of("[2.6-SNAPSHOT,)").select(versions), Version.of("3.0-alpha-1"))
versions = listOf("1.0", "1.1", "1.2", "1.2.3", "1.3", "1.4.2", "1.5-SNAPSHOT").map { Version.of(it) }
Assert.assertEquals(Version.of("[1.2,1.2.3)").select(versions), Version.of("1.2"))
Assert.assertEquals(Version.of("[1.2,1.2.3]").select(versions), Version.of("1.2.3"))
}
}