Dependency versions mapping support

If a dependency is fist managed by the service and later on by Spring
Boot, there is no way to keep a single entry that would change the
dependency according to the Spring Boot version.

A hack would consist of creating two entries that would have an exclusive
range but it's far from ideal.

This commit adds a `versions` attribute on each dependency that can
define an arbitrary number of mappings between a version and a range. If
one of those range matches, the related version is used (and the absence
of version means that no version need to be included). If no mapping
matched (or if there are no mappings at all), the default version is
used.

Closes gh-168
This commit is contained in:
Stephane Nicoll
2015-12-11 16:46:45 +01:00
parent 5fb8c84e5e
commit 143af6455e
6 changed files with 124 additions and 33 deletions

View File

@@ -84,6 +84,8 @@ class ProjectRequest {
*/
void resolve(InitializrMetadata metadata) {
List<String> depIds = style ? style : dependencies
String actualBootVersion = bootVersion ?: metadata.bootVersions.default.id
Version requestedVersion = Version.parse(actualBootVersion)
resolvedDependencies = depIds.collect {
def dependency = metadata.dependencies.get(it)
if (dependency == null) {
@@ -94,10 +96,8 @@ class ProjectRequest {
dependency = new Dependency()
dependency.asSpringBootStarter(it)
}
dependency
dependency.resolve(requestedVersion)
}
String actualBootVersion = bootVersion ?: metadata.bootVersions.default.id
Version requestedVersion = Version.parse(actualBootVersion)
Set<String> bomIds = []
resolvedDependencies.each {
it.facets.each {

View File

@@ -17,8 +17,11 @@
package io.spring.initializr.metadata
import com.fasterxml.jackson.annotation.JsonInclude
import groovy.transform.AutoClone
import groovy.transform.AutoCloneStyle
import groovy.transform.ToString
import io.spring.initializr.util.InvalidVersionException
import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionRange
/**
@@ -29,7 +32,8 @@ import io.spring.initializr.util.VersionRange
* @since 1.0
*/
@ToString(ignoreNulls = true, includePackage = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR)
class Dependency extends MetadataElement {
static final String SCOPE_COMPILE = 'compile'
@@ -51,8 +55,18 @@ class Dependency extends MetadataElement {
String artifactId
/**
* The default version, can be {@code null} to indicate that the
* version is managed by the project and does not need to be specified.
*/
String version
/**
* Versions mapping if the version differs according to the Spring Boot
* version. If no mapping matches, {@code version} is used.
*/
List<Mapping> versions = []
String scope = SCOPE_COMPILE
String description
@@ -140,6 +154,29 @@ class Dependency extends MetadataElement {
"dependency with id '$id'")
}
}
versions.each {
try {
it.range = VersionRange.parse(it.versionRange)
} catch (InvalidVersionException ex) {
throw new InvalidInitializrMetadataException("Invalid version range $it.versionRange for $this", ex)
}
}
}
/**
* Resolve this instance according to the specified Spring Boot {@link Version}. Return
* a {@link Dependency} instance that has its state resolved against the specified version.
*/
Dependency resolve(Version bootVersion) {
for (Mapping mapping : versions) {
if (mapping.range.match(bootVersion)) {
def dependency = new Dependency(this)
dependency.version = mapping.version ? mapping.version : this.version
dependency.versions = null
return dependency
}
}
return this
}
/**
@@ -155,4 +192,14 @@ class Dependency extends MetadataElement {
id = sb.toString()
}
static class Mapping {
String versionRange
String version
private VersionRange range
}
}

View File

@@ -16,12 +16,16 @@
package io.spring.initializr.metadata
import groovy.transform.AutoClone
import groovy.transform.AutoCloneStyle
/**
* A basic meta-data element
*
* @author Stephane Nicoll
* @since 1.0
*/
@AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR)
class MetadataElement {
/**

View File

@@ -116,7 +116,7 @@ class ProjectRequestTests {
def metadata = InitializrMetadataTestBuilder.withDefaults()
.addDependencyGroup('code', 'org.foo:bar').build()
request.style << 'org.foo:acme' // does not exist and
request.style << 'org.foo:acme' // does not exist
thrown.expect(InvalidProjectRequestException)
thrown.expectMessage('org.foo:acme')
@@ -154,6 +154,29 @@ class ProjectRequestTests {
request.resolve(metadata)
}
@Test
void resolveDependencyVersion() {
def dependency = createDependency('org.foo', 'bar', '1.2.0.RELEASE')
dependency.versions << new Dependency.Mapping(
version: '0.1.0.RELEASE', versionRange: '[1.0.0.RELEASE, 1.1.0.RELEASE)')
dependency.versions << new Dependency.Mapping(
version: '0.2.0.RELEASE', versionRange: '1.1.0.RELEASE')
def metadata = InitializrMetadataTestBuilder.withDefaults()
.addDependencyGroup('code', dependency).build()
def request = new ProjectRequest()
request.bootVersion = '1.0.5.RELEASE'
request.style << 'org.foo:bar'
request.resolve(metadata)
assertDependency(request.resolvedDependencies[0], 'org.foo', 'bar', '0.1.0.RELEASE')
def anotherRequest = new ProjectRequest()
anotherRequest.bootVersion = '1.1.0.RELEASE'
anotherRequest.style << 'org.foo:bar'
anotherRequest.resolve(metadata)
assertDependency(anotherRequest.resolvedDependencies[0], 'org.foo', 'bar', '0.2.0.RELEASE')
}
@Test
void resolveBuild() {
def request = new ProjectRequest()

View File

@@ -16,12 +16,14 @@
package io.spring.initializr.metadata
import io.spring.initializr.util.Version
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNull
import static org.junit.Assert.assertSame
/**
* @author Stephane Nicoll
@@ -136,4 +138,47 @@ class DependencyTests {
dependency.generateId()
}
@Test
void resolveNoMapping() {
def dependency = new Dependency(id: 'web')
dependency.resolve()
assertSame dependency, dependency.resolve(Version.parse('1.2.0.RELEASE'))
}
@Test
void resolveInvalidMapping() {
def dependency = new Dependency(id: 'web')
dependency.versions << new Dependency.Mapping(
versionRange: 'foo-bar', version: '0.1.0.RELEASE')
thrown.expect(InvalidInitializrMetadataException)
thrown.expectMessage('foo-bar')
dependency.resolve()
}
@Test
void resolveMatchingMapping() {
def dependency = new Dependency(id: 'web', description: 'A web dependency', version: '0.3.0.RELEASE',
keywords: ['foo', 'bar'], aliases: ['the-web'], facets: ['web'] )
dependency.versions << new Dependency.Mapping(
versionRange: '[1.1.0.RELEASE, 1.2.0.RELEASE)', version: '0.1.0.RELEASE')
dependency.versions << new Dependency.Mapping(
versionRange: '[1.2.0.RELEASE, 1.3.0.RELEASE)', version: '0.2.0.RELEASE')
dependency.resolve()
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.5.RELEASE')), '0.1.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('1.2.0.RELEASE')), '0.2.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('2.1.3.M1')), '0.3.0.RELEASE') // default
}
static void validateResolvedWebDependency(def dependency, def expectedVersion) {
assertEquals expectedVersion, dependency.version
assertEquals 'web', dependency.id
assertEquals 'org.springframework.boot', dependency.groupId
assertEquals 'spring-boot-starter-web', dependency.artifactId
assertEquals 2, dependency.keywords.size()
assertEquals 1, dependency.aliases.size()
assertEquals 1, dependency.facets.size()
}
}

View File

@@ -57,9 +57,6 @@
{
"content": [
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "spring-boot-starter-web",
"description": "Web dependency description",
@@ -70,12 +67,8 @@
"scope": "compile"
},
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "spring-boot-starter-security",
"facets": [],
"groupId": "org.springframework.boot",
"id": "security",
"name": "Security",
@@ -83,11 +76,8 @@
},
{
"aliases": ["jpa"],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "spring-boot-starter-data-jpa",
"facets": [],
"groupId": "org.springframework.boot",
"id": "data-jpa",
"name": "Data JPA",
@@ -99,9 +89,7 @@
{
"content": [
{
"aliases": [],
"artifactId": "foo",
"facets": [],
"groupId": "org.acme",
"id": "org.acme:foo",
"name": "Foo",
@@ -112,12 +100,8 @@
"version": "1.3.5"
},
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "bar",
"facets": [],
"groupId": "org.acme",
"id": "org.acme:bar",
"name": "Bar",
@@ -125,12 +109,8 @@
"version": "2.1.0"
},
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "biz",
"facets": [],
"groupId": "org.acme",
"id": "org.acme:biz",
"name": "Biz",
@@ -139,12 +119,8 @@
"versionRange": "1.2.0.BUILD-SNAPSHOT"
},
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "bur",
"facets": [],
"groupId": "org.acme",
"id": "org.acme:bur",
"name": "Bur",
@@ -153,12 +129,8 @@
"versionRange": "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)"
},
{
"aliases": [],
"keywords": [],
"weight": 0,
"starter": true,
"artifactId": "my-api",
"facets": [],
"groupId": "org.acme",
"id": "my-api",
"name": "My API",