From 143af6455e4e0b11681d3d1a2697fc98b8b04e30 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 11 Dec 2015 16:46:45 +0100 Subject: [PATCH] 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 --- .../generator/ProjectRequest.groovy | 6 +-- .../initializr/metadata/Dependency.groovy | 49 ++++++++++++++++++- .../metadata/MetadataElement.groovy | 4 ++ .../generator/ProjectRequestTests.groovy | 25 +++++++++- .../metadata/DependencyTests.groovy | 45 +++++++++++++++++ .../metadata/config/test-default.json | 28 ----------- 6 files changed, 124 insertions(+), 33 deletions(-) diff --git a/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy b/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy index 138dfc6a..327af8a5 100644 --- a/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/generator/ProjectRequest.groovy @@ -84,6 +84,8 @@ class ProjectRequest { */ void resolve(InitializrMetadata metadata) { List 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 bomIds = [] resolvedDependencies.each { it.facets.each { diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy index ea391f65..b3620d24 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy @@ -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 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 + + } + } diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/MetadataElement.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/MetadataElement.groovy index e80c4365..26e85f4b 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/MetadataElement.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/MetadataElement.groovy @@ -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 { /** diff --git a/initializr/src/test/groovy/io/spring/initializr/generator/ProjectRequestTests.groovy b/initializr/src/test/groovy/io/spring/initializr/generator/ProjectRequestTests.groovy index 3775b74e..f866e09c 100644 --- a/initializr/src/test/groovy/io/spring/initializr/generator/ProjectRequestTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/generator/ProjectRequestTests.groovy @@ -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() diff --git a/initializr/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy b/initializr/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy index 24f977fe..b8fa122c 100644 --- a/initializr/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy @@ -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() + + } + } diff --git a/initializr/src/test/resources/metadata/config/test-default.json b/initializr/src/test/resources/metadata/config/test-default.json index 12347881..2455670b 100644 --- a/initializr/src/test/resources/metadata/config/test-default.json +++ b/initializr/src/test/resources/metadata/config/test-default.json @@ -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",