Support of additional BOM

BOMs are structured in such a way than adding one to the project may
require another one(s) to be added to the project.

This commit adds an `additionalBoms` property to `BillOfMaterials` that
can be used to refer the BOM(s) that should be automatically added if
said BOM is added to the project.

Closes gh-190
This commit is contained in:
Stephane Nicoll 2016-02-02 21:00:05 +01:00
parent 9eeb1df4ba
commit 5faf874bac
10 changed files with 304 additions and 23 deletions

View File

@ -61,7 +61,7 @@ class ProjectRequest {
// Resolved dependencies based on the ids provided by either "style" or "dependencies"
List<Dependency> resolvedDependencies
final List<BillOfMaterials> boms = []
final Map<String, BillOfMaterials> boms = [:]
final Map<String, Repository> repositories = [:]
@ -98,7 +98,6 @@ class ProjectRequest {
}
dependency.resolve(requestedVersion)
}
Set<String> bomIds = []
resolvedDependencies.each {
it.facets.each {
if (!facets.contains(it)) {
@ -113,11 +112,7 @@ class ProjectRequest {
}
}
if (it.bom) {
String bomId = it.bom
if (!bomIds.contains(bomId)) {
bomIds << bomId
boms << metadata.configuration.env.boms[bomId].resolve(requestedVersion)
}
resolveBom(metadata, it.bom, requestedVersion)
}
if (it.repository) {
String repositoryId = it.repository
@ -169,7 +164,7 @@ class ProjectRequest {
repositories['spring-snapshots'] = metadata.configuration.env.repositories['spring-snapshots']
repositories['spring-milestones'] = metadata.configuration.env.repositories['spring-milestones']
}
boms.each {
boms.values().each {
it.repositories.each {
if (!repositories[it]) {
repositories[it] = metadata.configuration.env.repositories[it]
@ -178,6 +173,16 @@ class ProjectRequest {
}
}
private void resolveBom(InitializrMetadata metadata, String bomId, Version requestedVersion) {
if (!boms[bomId]) {
def bom = metadata.configuration.env.boms[bomId].resolve(requestedVersion)
boms[bomId] = bom
bom.additionalBoms.each { id ->
resolveBom(metadata, id, requestedVersion)
}
}
}
/**
* Update this request once it has been resolved with the specified {@link InitializrMetadata}.
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -37,12 +37,22 @@ class BillOfMaterials {
String artifactId
/**
* The version of the BOM. Can be {@code null} if it is provided via
* a mapping.
* The version of the BOM. Can be {@code null} if it is provided via a mapping.
*/
String version
/**
* The BOM(s) that should be automatically included if this BOM is required. Can be
* {@code null} if it is provided via a mapping.
*/
List<String> additionalBoms = []
/**
* The repositories that are required if this BOM is required. Can be {@code null} if
* it is provided via a mapping.
*/
List<String> repositories = []
final List<Mapping> mappings = []
void validate() {
@ -60,8 +70,8 @@ class BillOfMaterials {
/**
* Resolve this instance according to the specified Spring Boot {@link Version}. Return
* a {@link BillOfMaterials} instance that holds the version and repositories to use, if
* any.
* a {@link BillOfMaterials} instance that holds the version, repositories and
* additional BOMs to use, if any.
*/
BillOfMaterials resolve(Version bootVersion) {
if (!mappings) {
@ -71,8 +81,9 @@ class BillOfMaterials {
for (Mapping mapping : mappings) {
if (mapping.range.match(bootVersion)) {
def resolvedBom = new BillOfMaterials(groupId: groupId, artifactId: artifactId,
version: mapping.version, repositories: repositories)
resolvedBom.repositories += mapping.repositories
version: mapping.version)
resolvedBom.repositories += mapping.repositories ?: repositories
resolvedBom.additionalBoms += mapping.additionalBoms ?: additionalBoms
return resolvedBom
}
}
@ -88,6 +99,8 @@ class BillOfMaterials {
List<String> repositories = []
List<String> additionalBoms = []
private VersionRange range
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -91,8 +91,8 @@ class InitializrMetadata {
dependencies.validate()
def repositories = configuration.env.repositories
def boms = configuration.env.boms
for (Dependency dependency : dependencies.all) {
def boms = configuration.env.boms
if (dependency.bom && !boms[dependency.bom]) {
throw new InvalidInitializrMetadataException("Dependency $dependency " +
"defines an invalid BOM id $dependency.bom, available boms $boms")
@ -103,19 +103,32 @@ class InitializrMetadata {
"defines an invalid repository id $dependency.repository, available repositores $repositories")
}
}
for (BillOfMaterials bom : configuration.env.boms.values()) {
for (BillOfMaterials bom : boms.values()) {
for (String r : bom.repositories) {
if (!repositories[r]) {
throw new InvalidInitializrMetadataException("$bom " +
"defines an invalid repository id $r, available repositores $repositories")
}
}
for (String b : bom.additionalBoms) {
if (!boms[b]) {
throw new InvalidInitializrMetadataException("$bom defines an invalid " +
"additional bom id $b, available boms $boms")
}
}
for (BillOfMaterials.Mapping m : bom.mappings) {
for (String r : m.repositories) {
if (!repositories[r]) {
throw new InvalidInitializrMetadataException("$m of $bom " +
"defines an invalid repository id $r, available repositores $repositories")
}
}
for (String b : m.additionalBoms) {
if (!boms[b]) {
throw new InvalidInitializrMetadataException("$m of $bom defines " +
"an invalid additional bom id $b, available boms $boms")
}
}
}
}

View File

@ -48,7 +48,7 @@ dependencies {<% compileDependencies.each { %>
}
<% if (boms) { %>
dependencyManagement {
imports { <% boms.each { %>
imports { <% boms.values().each { %>
mavenBom "${it.groupId}:${it.artifactId}${it.version ? ":$it.version" : ""}" <% } %>
}
}

View File

@ -60,7 +60,7 @@
</dependencies>
<% if (boms) { %>
<dependencyManagement>
<dependencies><% boms.each { %>
<dependencies><% boms.values().each { %>
<dependency>
<groupId>${it.groupId}</groupId>
<artifactId>${it.artifactId}</artifactId>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package io.spring.initializr.generator
import io.spring.initializr.metadata.BillOfMaterials
import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadataBuilder
import io.spring.initializr.test.InitializrMetadataTestBuilder
@ -232,7 +233,7 @@ class ProjectRequestTests {
void resolveApplicationNameWithApplicationNameSet() {
def request = new ProjectRequest()
request.name = 'Foo2'
request.applicationName ='MyApplicationName'
request.applicationName = 'MyApplicationName'
def metadata = InitializrMetadataTestBuilder.withDefaults().build()
request.resolve(metadata)
@ -258,6 +259,100 @@ class ProjectRequestTests {
assertEquals 'com.foo.bar', request.packageName
}
@Test
void resolveAdditionalBoms() {
def request = new ProjectRequest()
def dependency = new Dependency(id: 'foo', bom: 'foo-bom')
def bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'foo-bom',
version: '1.0.0', additionalBoms: ['bar-bom'])
def additionalBom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bar-bom', version: '1.1.0')
def metadata = InitializrMetadataTestBuilder
.withDefaults()
.addBom('foo-bom', bom)
.addBom('bar-bom', additionalBom)
.addDependencyGroup('test', dependency)
.build()
request.style << 'foo'
request.resolve(metadata)
assertEquals(1, request.resolvedDependencies.size())
assertEquals 2, request.boms.size()
assertEquals bom, request.boms['foo-bom']
assertEquals additionalBom, request.boms['bar-bom']
}
@Test
void resolveAdditionalBomsDuplicates() {
def request = new ProjectRequest()
def dependency = new Dependency(id: 'foo', bom: 'foo-bom')
def anotherDependency = new Dependency(id: 'bar', bom: 'bar-bom')
def bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'foo-bom',
version: '1.0.0', additionalBoms: ['bar-bom'])
def additionalBom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bar-bom', version: '1.1.0')
def metadata = InitializrMetadataTestBuilder
.withDefaults()
.addBom('foo-bom', bom)
.addBom('bar-bom', additionalBom)
.addDependencyGroup('test', dependency, anotherDependency)
.build()
request.style << 'foo' << 'bar'
request.resolve(metadata)
assertEquals(2, request.resolvedDependencies.size())
assertEquals 2, request.boms.size()
assertEquals bom, request.boms['foo-bom']
assertEquals additionalBom, request.boms['bar-bom']
}
@Test
void resolveAdditionalRepositories() {
def request = new ProjectRequest()
def dependency = new Dependency(id: 'foo', bom: 'foo-bom', repository: 'foo-repo')
def bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'foo-bom',
version: '1.0.0', repositories: ['bar-repo'])
def metadata = InitializrMetadataTestBuilder
.withDefaults()
.addBom('foo-bom', bom)
.addRepository('foo-repo', 'foo-repo', 'http://example.com/foo', false)
.addRepository('bar-repo', 'bar-repo', 'http://example.com/bar', false)
.addDependencyGroup('test', dependency)
.build()
request.style << 'foo'
request.resolve(metadata)
assertEquals(1, request.resolvedDependencies.size())
assertEquals 1, request.boms.size()
assertEquals 2, request.repositories.size()
assertEquals metadata.configuration.env.repositories['foo-repo'],
request.repositories['foo-repo']
assertEquals metadata.configuration.env.repositories['bar-repo'],
request.repositories['bar-repo']
}
@Test
void resolveAdditionalRepositoriesDuplicates() {
def request = new ProjectRequest()
def dependency = new Dependency(id: 'foo', bom: 'foo-bom', repository: 'foo-repo')
def anotherDependency = new Dependency(id: 'bar', repository: 'bar-repo')
def bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'foo-bom',
version: '1.0.0', repositories: ['bar-repo'])
def metadata = InitializrMetadataTestBuilder
.withDefaults()
.addBom('foo-bom', bom)
.addRepository('foo-repo', 'foo-repo', 'http://example.com/foo', false)
.addRepository('bar-repo', 'bar-repo', 'http://example.com/bar', false)
.addDependencyGroup('test', dependency, anotherDependency)
.build()
request.style << 'foo' << 'bar'
request.resolve(metadata)
assertEquals(2, request.resolvedDependencies.size())
assertEquals 1, request.boms.size()
assertEquals 2, request.repositories.size()
assertEquals metadata.configuration.env.repositories['foo-repo'],
request.repositories['foo-repo']
assertEquals metadata.configuration.env.repositories['bar-repo'],
request.repositories['bar-repo']
}
private static void assertBootStarter(Dependency actual, String name) {
def expected = new Dependency()
expected.asSpringBootStarter(name)

View File

@ -0,0 +1,93 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.hamcrest.CoreMatchers.equalTo
import static org.hamcrest.CoreMatchers.sameInstance
import static org.junit.Assert.assertThat
/**
* @author Stephane Nicoll
*/
class BillOfMaterialsTests {
@Rule
public final ExpectedException thrown = ExpectedException.none()
@Test
void resolveSimpleBom() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bom', version: '1.0.0')
bom.validate()
BillOfMaterials resolved = bom.resolve(Version.parse('1.2.3.RELEASE'))
assertThat(bom, sameInstance(resolved))
}
@Test
void resolveSimpleRange() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'bom',
version: '1.0.0', repositories: ['repo-main'], additionalBoms: ['bom-main'])
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.2.0.RELEASE,1.3.0.M1)',
version: '1.1.0')
bom.validate()
BillOfMaterials resolved = bom.resolve(Version.parse('1.2.3.RELEASE'))
assertThat(resolved.groupId, equalTo('com.example'))
assertThat(resolved.artifactId, equalTo('bom'))
assertThat(resolved.version, equalTo('1.1.0'))
assertThat(resolved.repositories.size(), equalTo(1))
assertThat(resolved.repositories[0], equalTo('repo-main'))
assertThat(resolved.additionalBoms.size(), equalTo(1))
assertThat(resolved.additionalBoms[0], equalTo('bom-main'))
}
@Test
void resolveRangeOverride() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bom', version: '1.0.0', repositories: ['repo-main'], additionalBoms: ['bom-main'])
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.2.0.RELEASE,1.3.0.M1)',
version: '1.1.0', repositories: ['repo-foo'], additionalBoms: ['bom-foo'])
bom.validate()
BillOfMaterials resolved = bom.resolve(Version.parse('1.2.3.RELEASE'))
assertThat(resolved.groupId, equalTo('com.example'))
assertThat(resolved.artifactId, equalTo('bom'))
assertThat(resolved.version, equalTo('1.1.0'))
assertThat(resolved.repositories.size(), equalTo(1))
assertThat(resolved.repositories[0], equalTo('repo-foo'))
assertThat(resolved.additionalBoms.size(), equalTo(1))
assertThat(resolved.additionalBoms[0], equalTo('bom-foo'))
}
@Test
void noRangeAvailable() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'bom')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.2.0.RELEASE,1.3.0.M1)',
version: '1.1.0')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.3.0.M1,1.4.0.M1)',
version: '1.2.0')
bom.validate()
thrown.expect(IllegalStateException)
thrown.expectMessage('1.4.1.RELEASE')
bom.resolve(Version.parse('1.4.1.RELEASE'))
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -82,6 +82,22 @@ class InitializrMetadataTests {
builder.build()
}
@Test
void invalidBomUnknownAdditionalBom() {
def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom',
version: '1.0.0.RELEASE', additionalBoms: ['bar-bom', 'biz-bom'])
def barBom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'bar-bom',
version: '1.0.0.RELEASE')
InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder
.withDefaults().addBom('foo-bom', bom).addBom('bar-bom', barBom)
thrown.expect(InvalidInitializrMetadataException)
thrown.expectMessage("invalid additional bom")
thrown.expectMessage("biz-bom")
builder.build()
}
@Test
void invalidBomVersionRangeMapping() {
def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom')
@ -113,4 +129,21 @@ class InitializrMetadataTests {
builder.build()
}
@Test
void invalidBomVersionRangeMappingUnknownAdditionalBom() {
def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.0.0.RELEASE,1.3.0.M1)', version: '1.0.0')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '1.3.0.M2', version: '1.2.0',
additionalBoms: ['bar-bom'])
InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder
.withDefaults().addBom('foo-bom', bom)
thrown.expect(InvalidInitializrMetadataException)
thrown.expectMessage("invalid additional bom")
thrown.expectMessage('1.3.0.M2')
thrown.expectMessage("bar-bom")
builder.build()
}
}

View File

@ -8,6 +8,7 @@ initializr:
my-api-bom:
groupId: org.acme
artifactId: my-api-bom
additionalBoms: ['my-api-dependencies-bom']
mappings:
- versionRange: "[1.0.0.RELEASE,1.1.6.RELEASE)"
version: 1.0.0.RELEASE
@ -15,6 +16,11 @@ initializr:
- versionRange: "1.2.1.RELEASE"
version: 2.0.0.RELEASE
repositories: my-api-repo-2
my-api-dependencies-bom:
groupId: org.acme
artifactId: my-api-dependencies-bom
version: 1.0.0.RELEASE
repositories: my-api-repo-3
repositories:
my-api-repo-1:
name: repo1
@ -22,6 +28,9 @@ initializr:
my-api-repo-2:
name: repo2
url: http://example.com/repo2
my-api-repo-3:
name: repo3
url: http://example.com/repo3
dependencies:
- name: Core
content:

View File

@ -52,6 +52,11 @@
"url": "http://example.com/repo2",
"snapshotsEnabled": false
},
"my-api-repo-3": {
"name": "repo3",
"url": "http://example.com/repo3",
"snapshotsEnabled": false
},
"spring-milestones": {
"name": "Spring Milestones",
"snapshotsEnabled": false,
@ -68,12 +73,16 @@
"groupId": "org.acme",
"artifactId": "my-api-bom",
"repositories": [],
"additionalBoms": [
"my-api-dependencies-bom"
],
"mappings": [
{
"versionRange": "[1.0.0.RELEASE,1.1.6.RELEASE)",
"repositories": [
"my-api-repo-1"
],
"additionalBoms": [],
"version": "1.0.0.RELEASE"
},
{
@ -81,9 +90,20 @@
"repositories": [
"my-api-repo-2"
],
"additionalBoms": [],
"version": "2.0.0.RELEASE"
}
]
},
"my-api-dependencies-bom": {
"groupId": "org.acme",
"repositories": [
"my-api-repo-3"
],
"mappings": [],
"artifactId": "my-api-dependencies-bom",
"additionalBoms": [],
"version": "1.0.0.RELEASE"
}
},
"springBootMetadataUrl": "https://spring.io/project_metadata/spring-boot"