Auto-updatable version ranges

This commit improves the version format so that the minor and patch
elements can hold a special 'x' character besides the version, i.e.
`1.x.x.RELEASE` or `2.4.x.BUILD-SNAPSHOT`. A `VersionParser` now takes
care to resolve those against a list of known Spring Boot versions.

This is particularly useful in version ranges that have to change when
the latest Spring Boot versions change. Spring Initializr already auto-
udpates itself based on the sagan metadata. When a range is using this
feature, it is also automatically updated.

It might be hard to track the actual range values on a given instance so
an `InfoContributor` is now automatically exposed to list them.

Closes gh-328
This commit is contained in:
Stephane Nicoll 2016-12-06 16:39:23 +01:00
parent 814a4ad260
commit 827b9d6e93
22 changed files with 673 additions and 148 deletions

View File

@ -0,0 +1,40 @@
/*
* 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.actuate.autoconfigure
import io.spring.initializr.actuate.info.BomRangesInfoContributor
import io.spring.initializr.metadata.InitializrMetadataProvider
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} to improve actuator endpoints with initializr specific information.
*
* @author Stephane Nicoll
*/
@Configuration
class InitializrActuatorEndpointsAutoConfiguration {
@Bean
BomRangesInfoContributor bomRangesInfoContributor(
InitializrMetadataProvider metadataProvider) {
return new BomRangesInfoContributor(metadataProvider)
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.actuate.info
import io.spring.initializr.metadata.InitializrMetadataProvider
import org.springframework.boot.actuate.info.Info
import org.springframework.boot.actuate.info.InfoContributor
/**
* An {@link InfoContributor} that exposes the actual ranges used by each bom
* defined in the project.
*
* @author Stephane Nicoll
*/
class BomRangesInfoContributor implements InfoContributor {
private final InitializrMetadataProvider metadataProvider
BomRangesInfoContributor(InitializrMetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider
}
@Override
void contribute(Info.Builder builder) {
def details = [:]
metadataProvider.get().configuration.env.boms.each { k, v ->
if (v.mappings) {
def bom = [:]
v.mappings.each {
String requirement = "Spring Boot ${it.determineVersionRangeRequirement()}"
bom[it.version] = requirement
}
details[k] = bom
}
}
if (details) {
builder.withDetail('bom-ranges', details)
}
}
}

View File

@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.spring.initializr.actuate.autoconfigure.InitializrActuatorEndpointsAutoConfiguration,\
io.spring.initializr.actuate.autoconfigure.InitializrStatsAutoConfiguration,\ io.spring.initializr.actuate.autoconfigure.InitializrStatsAutoConfiguration,\
io.spring.initializr.actuate.autoconfigure.InitializrMetricsConfiguration io.spring.initializr.actuate.autoconfigure.InitializrMetricsConfiguration

View File

@ -0,0 +1,82 @@
/*
* 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.actuate.info
import io.spring.initializr.metadata.BillOfMaterials
import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.metadata.SimpleInitializrMetadataProvider
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
import org.junit.Test
import org.springframework.boot.actuate.info.Info
import static org.assertj.core.api.Assertions.assertThat
import static org.assertj.core.api.Assertions.entry
/**
* Tests for {@link BomRangesInfoContributor}
*
* @author Stephane Nicoll
*/
class BomRangesInfoContributorTests {
@Test
void noBom() {
def metadata = InitializrMetadataTestBuilder.withDefaults().build()
def info = getInfo(metadata)
assertThat(info.details).doesNotContainKeys('bom-ranges')
}
@Test
void noMapping() {
def bom = new BillOfMaterials(groupId: 'com.example', artifactId: 'bom', version: '1.0.0')
def metadata = InitializrMetadataTestBuilder.withDefaults()
.addBom('foo', bom)
.build()
def info = getInfo(metadata)
assertThat(info.details).doesNotContainKeys('bom-ranges')
}
@Test
void withMappings() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bom', version: '1.0.0')
bom.mappings << new BillOfMaterials.Mapping(
versionRange: '[1.3.0.RELEASE,1.3.8.RELEASE]', version: '1.1.0')
bom.mappings << new BillOfMaterials.Mapping(
versionRange: '1.3.8.BUILD-SNAPSHOT', version: '1.1.1-SNAPSHOT')
def metadata = InitializrMetadataTestBuilder.withDefaults()
.addBom('foo', bom)
.build()
def info = getInfo(metadata)
assertThat(info.details).containsKeys('bom-ranges')
Map<String,Object> ranges = info.details['bom-ranges'] as Map<String, Object>
assertThat(ranges).containsOnlyKeys('foo')
Map<String,Object> foo = ranges['foo'] as Map<String, Object>
assertThat(foo).containsExactly(
entry('1.1.0', 'Spring Boot >=1.3.0.RELEASE and <=1.3.8.RELEASE'),
entry('1.1.1-SNAPSHOT', 'Spring Boot >=1.3.8.BUILD-SNAPSHOT'))
}
private static Info getInfo(InitializrMetadata metadata) {
Info.Builder builder = new Info.Builder()
new BomRangesInfoContributor(new SimpleInitializrMetadataProvider(metadata))
.contribute(builder)
builder.build()
}
}

View File

@ -19,7 +19,6 @@ package io.spring.initializr.generator
import io.spring.initializr.metadata.InitializrMetadata import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.metadata.Type import io.spring.initializr.metadata.Type
import io.spring.initializr.util.GroovyTemplate import io.spring.initializr.util.GroovyTemplate
import io.spring.initializr.util.VersionRange
/** /**
* Generate help pages for command-line clients. * Generate help pages for command-line clients.
@ -144,7 +143,7 @@ class CommandLineHelpGenerator {
String[] data = new String[3] String[] data = new String[3]
data[0] = dep.id data[0] = dep.id
data[1] = dep.description ?: dep.name data[1] = dep.description ?: dep.name
data[2] = buildVersionRangeRepresentation(dep.versionRange) data[2] = dep.versionRequirement
dependencyTable[i + 1] = data dependencyTable[i + 1] = data
} }
TableGenerator.generate(dependencyTable) TableGenerator.generate(dependencyTable)
@ -182,18 +181,6 @@ class CommandLineHelpGenerator {
result result
} }
private static String buildVersionRangeRepresentation(String range) {
if (!range) {
return null
}
VersionRange versionRange = VersionRange.parse(range)
if (versionRange.higherVersion == null) {
return ">= $range"
} else {
return range.trim()
}
}
private static String buildTagRepresentation(Type type) { private static String buildTagRepresentation(Type type) {
if (type.tags.isEmpty()) { if (type.tags.isEmpty()) {
return ""; return "";

View File

@ -93,12 +93,9 @@ class ProjectRequest extends BasicProjectRequest {
facets.add(it) facets.add(it)
} }
} }
if (it.versionRange) { if (!it.match(requestedVersion)) {
def range = VersionRange.parse(it.versionRange) throw new InvalidProjectRequestException("Dependency '$it.id' is not compatible " +
if (!range.match(requestedVersion)) { "with Spring Boot $requestedVersion")
throw new InvalidProjectRequestException("Dependency '$it.id' is not compatible " +
"with Spring Boot $bootVersion")
}
} }
if (it.bom) { if (it.bom) {
resolveBom(metadata, it.bom, requestedVersion) resolveBom(metadata, it.bom, requestedVersion)

View File

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonInclude
import groovy.transform.ToString import groovy.transform.ToString
import io.spring.initializr.util.InvalidVersionException import io.spring.initializr.util.InvalidVersionException
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionParser
import io.spring.initializr.util.VersionRange import io.spring.initializr.util.VersionRange
/** /**
@ -75,9 +76,13 @@ class BillOfMaterials {
if (!version && !mappings) { if (!version && !mappings) {
throw new InvalidInitializrMetadataException("No version available for $this"); throw new InvalidInitializrMetadataException("No version available for $this");
} }
updateVersionRange(VersionParser.DEFAULT)
}
void updateVersionRange(VersionParser versionParser) {
mappings.each { mappings.each {
try { try {
it.range = VersionRange.parse(it.versionRange) it.range = versionParser.parseRange(it.versionRange)
} catch (InvalidVersionException ex) { } catch (InvalidVersionException ex) {
throw new InvalidInitializrMetadataException("Invalid version range $it.versionRange for $this", ex) throw new InvalidInitializrMetadataException("Invalid version range $it.versionRange for $this", ex)
} }
@ -119,6 +124,10 @@ class BillOfMaterials {
private VersionRange range private VersionRange range
String determineVersionRangeRequirement() {
range.toString()
}
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
package io.spring.initializr.metadata package io.spring.initializr.metadata
import io.spring.initializr.util.VersionParser
/** /**
* A {@link ServiceCapability} listing the available dependencies defined as a * A {@link ServiceCapability} listing the available dependencies defined as a
* {@link ServiceCapabilityType#HIERARCHICAL_MULTI_SELECT} capability. * {@link ServiceCapabilityType#HIERARCHICAL_MULTI_SELECT} capability.
@ -53,6 +55,12 @@ class DependenciesCapability extends ServiceCapability<List<DependencyGroup>> {
index() index()
} }
void updateVersionRange(VersionParser versionParser) {
indexedDependencies.values().each {
it.updateVersionRanges(versionParser)
}
}
@Override @Override
void merge(List<DependencyGroup> otherContent) { void merge(List<DependencyGroup> otherContent) {
otherContent.each { group -> otherContent.each { group ->

View File

@ -23,6 +23,7 @@ import groovy.transform.AutoCloneStyle
import groovy.transform.ToString import groovy.transform.ToString
import io.spring.initializr.util.InvalidVersionException import io.spring.initializr.util.InvalidVersionException
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionParser
import io.spring.initializr.util.VersionRange import io.spring.initializr.util.VersionRange
/** /**
@ -84,6 +85,8 @@ class Dependency extends MetadataElement {
@JsonIgnore @JsonIgnore
String versionRequirement String versionRequirement
private VersionRange range
String bom String bom
@ -159,18 +162,22 @@ class Dependency extends MetadataElement {
"Invalid dependency, id should have the form groupId:artifactId[:version] but got $id") "Invalid dependency, id should have the form groupId:artifactId[:version] but got $id")
} }
} }
updateVersionRanges(VersionParser.DEFAULT)
}
def updateVersionRanges(VersionParser versionParser) {
if (versionRange) { if (versionRange) {
try { try {
def range = VersionRange.parse(versionRange) range = versionParser.parseRange(versionRange)
versionRequirement = range.toString() versionRequirement = range.toString()
} catch (InvalidVersionException ex) { } catch (InvalidVersionException ex) {
throw new InvalidInitializrMetadataException("Invalid version range '$versionRange' for " + throw new InvalidInitializrMetadataException("Invalid version range '$versionRange' for " +
"dependency with id '$id'") "dependency with id '$id'", ex)
} }
} }
mappings.each { mappings.each {
try { try {
it.range = VersionRange.parse(it.versionRange) it.range = versionParser.parseRange(it.versionRange)
} catch (InvalidVersionException ex) { } catch (InvalidVersionException ex) {
throw new InvalidInitializrMetadataException("Invalid version range $it.versionRange for $this", ex) throw new InvalidInitializrMetadataException("Invalid version range $it.versionRange for $this", ex)
} }
@ -200,8 +207,8 @@ class Dependency extends MetadataElement {
* Specify if this dependency is available for the specified Spring Boot version. * Specify if this dependency is available for the specified Spring Boot version.
*/ */
boolean match(Version version) { boolean match(Version version) {
if (versionRange) { if (range) {
return VersionRange.parse(versionRange).match(version) return range.match(version)
} }
true true
} }

View File

@ -16,6 +16,9 @@
package io.spring.initializr.metadata package io.spring.initializr.metadata
import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionParser
/** /**
* Meta-data used to generate a project. * Meta-data used to generate a project.
* *
@ -132,7 +135,23 @@ class InitializrMetadata {
} }
} }
} }
}
/**
* Update the available Spring Boot versions with the specified capabilities.
* @param versionsMetadata the Spring Boot boot versions metadata to use
*/
void updateSpringBootVersions(List<DefaultMetadataElement> versionsMetadata) {
bootVersions.content.clear()
bootVersions.content.addAll(versionsMetadata)
List<Version> bootVersions = bootVersions.content.collect {
Version.parse(it.id)
}
VersionParser parser = new VersionParser(bootVersions)
dependencies.updateVersionRange(parser)
configuration.env.boms.values().each {
it.updateVersionRange(parser)
}
} }
/** /**

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,13 +44,22 @@ final class Version implements Serializable, Comparable<Version> {
private static final VersionQualifierComparator qualifierComparator = new VersionQualifierComparator() private static final VersionQualifierComparator qualifierComparator = new VersionQualifierComparator()
Integer major private static final VersionParser parser = new VersionParser(Collections.EMPTY_LIST)
Integer minor
Integer patch final Integer major
Qualifier qualifier final Integer minor
final Integer patch
final Qualifier qualifier
Version(Integer major, Integer minor, Integer patch, Qualifier qualifier) {
this.major = major
this.minor = minor
this.patch = patch
this.qualifier = qualifier
}
@Override @Override
public String toString() { String toString() {
"${major}.${minor}.${patch}" + (qualifier?".${qualifier.qualifier}${qualifier.version?:''}" : '') "${major}.${minor}.${patch}" + (qualifier?".${qualifier.qualifier}${qualifier.version?:''}" : '')
} }
@ -60,29 +69,10 @@ final class Version implements Serializable, Comparable<Version> {
* @param text the version text * @param text the version text
* @return a Version instance for the specified version text * @return a Version instance for the specified version text
* @throws InvalidVersionException if the version text could not be parsed * @throws InvalidVersionException if the version text could not be parsed
* @see #safeParse(java.lang.String) * @see {@link VersionParser}
*/ */
static Version parse(String text) { static Version parse(String text) {
Assert.notNull(text, 'Text must not be null') return parser.parse(text)
def matcher = (text.trim() =~ VERSION_REGEX)
if (!matcher.matches()) {
throw new InvalidVersionException("Could not determine version based on '$text': version format " +
"is Minor.Major.Patch.Qualifier (i.e. 1.0.5.RELEASE)")
}
Version version = new Version()
version.major = Integer.valueOf(matcher[0][1])
version.minor = Integer.valueOf(matcher[0][2])
version.patch = Integer.valueOf(matcher[0][3])
String qualifierId = matcher[0][4]
if (qualifierId) {
Qualifier qualifier = new Qualifier(qualifier: qualifierId)
String o = matcher[0][5]
if (o != null) {
qualifier.version = Integer.valueOf(o)
}
version.qualifier = qualifier
}
version
} }
/** /**
@ -91,7 +81,7 @@ final class Version implements Serializable, Comparable<Version> {
* Return {@code null} if the text represents an invalid version. * Return {@code null} if the text represents an invalid version.
* @param text the version text * @param text the version text
* @return a Version instance for the specified version text * @return a Version instance for the specified version text
* @see #parse(java.lang.String) * @see {@link VersionParser}
*/ */
static safeParse(String text) { static safeParse(String text) {
try { try {
@ -129,7 +119,7 @@ final class Version implements Serializable, Comparable<Version> {
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
public static class Qualifier { static class Qualifier {
String qualifier String qualifier
Integer version Integer version
} }

View File

@ -0,0 +1,143 @@
/*
* 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.util
import org.springframework.util.Assert
/**
* Parser for {@link Version} and {@link VersionRange} that allows to resolve the minor
* and patch value against a configurable list of "latest versions".
* <p>
* For example a parser that is configured with {@code 1.3.7.RELEASE} and
* {@code 1.4.2.RELEASE} as latest versions can parse {@code 1.3.x.RELEASE} to
* {@code 1.3.7.RELEASE}. Note that the qualifier is important here:
* {@code 1.3.8.BUILD-SNAPSHOT} would be parsed as {@code 1.3.999.BUILD-SNAPSHOT} as the
* parser doesn't know the latest {@code BUILD-SNAPSHOT} in the {@code 1.3.x} release
* line.
*
* @author Stephane Nicoll
*/
class VersionParser {
public static final VersionParser DEFAULT = new VersionParser(Collections.emptyList())
private static final String VERSION_REGEX = '^(\\d+)\\.(\\d+|x)\\.(\\d+|x)(?:\\.([^0-9]+)(\\d+)?)?$'
private static final String RANGE_REGEX = "(\\(|\\[)(.*),(.*)(\\)|\\])"
private final List<Version> latestVersions;
VersionParser(List<Version> latestVersions) {
this.latestVersions = latestVersions
}
/**
* Parse the string representation of a {@link Version}. Throws an
* {@link InvalidVersionException} if the version could not be parsed.
* @param text the version text
* @return a Version instance for the specified version text
* @throws InvalidVersionException if the version text could not be parsed
* @see #safeParse(java.lang.String)
*/
Version parse(String text) {
Assert.notNull(text, 'Text must not be null')
def matcher = (text.trim() =~ VERSION_REGEX)
if (!matcher.matches()) {
throw new InvalidVersionException("Could not determine version based on '$text': version format " +
"is Minor.Major.Patch.Qualifier (e.g. 1.0.5.RELEASE)")
}
Integer major = Integer.valueOf(matcher[0][1])
String minor = matcher[0][2]
String patch = matcher[0][3]
def qualifier = null;
String qualifierId = matcher[0][4]
if (qualifierId) {
qualifier = new Version.Qualifier(qualifier: qualifierId)
String o = matcher[0][5]
if (o != null) {
qualifier.version = Integer.valueOf(o)
}
}
if (minor == "x" || patch == "x") {
Integer minorInt = minor == "x" ? null : Integer.parseInt(minor)
Version latest = findLatestVersion(major, minorInt, qualifier)
if (!latest) {
return new Version(major, (minor == "x" ? 999 : Integer.parseInt(minor)),
(patch == "x" ? 999 : Integer.parseInt(patch)), qualifier)
}
return new Version(major, latest.minor, latest.patch, latest.qualifier)
} else {
return new Version(major, Integer.parseInt(minor), Integer.parseInt(patch), qualifier)
}
}
/**
* Parse safely the specified string representation of a {@link Version}.
* <p>
* Return {@code null} if the text represents an invalid version.
* @param text the version text
* @return a Version instance for the specified version text
* @see #parse(java.lang.String)
*/
Version safeParse(String text) {
try {
return parse(text)
} catch (InvalidVersionException ex) {
return null
}
}
/**
* Parse the string representation of a {@link VersionRange}. Throws an
* {@link InvalidVersionException} if the range could not be parsed.
* @param text the range text
* @return a VersionRange instance for the specified range text
* @throws InvalidVersionException if the range text could not be parsed
*/
VersionRange parseRange(String text) {
Assert.notNull(text, "Text must not be null")
def matcher = (text.trim() =~ RANGE_REGEX)
if (!matcher.matches()) {
// Try to read it as simple string
Version version = parse(text)
return new VersionRange(version, true, null, true)
}
boolean lowerInclusive = matcher[0][1].equals('[')
Version lowerVersion = parse(matcher[0][2])
Version higherVersion = parse(matcher[0][3])
boolean higherInclusive = matcher[0][4].equals(']')
new VersionRange(lowerVersion, lowerInclusive, higherVersion, higherInclusive)
}
private Version findLatestVersion(Integer major, Integer minor,
Version.Qualifier qualifier) {
def matches = this.latestVersions.findAll {
if (major && major != it.major) {
return false;
}
if (minor && minor != it.minor) {
return false;
}
if (qualifier && it.qualifier != qualifier) {
return false;
}
return true;
}
return (matches.size() == 1 ? matches[0] : null)
}
}

View File

@ -17,7 +17,6 @@
package io.spring.initializr.util package io.spring.initializr.util
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.springframework.util.Assert import org.springframework.util.Assert
@ -41,15 +40,13 @@ import org.springframework.util.Assert
@EqualsAndHashCode @EqualsAndHashCode
class VersionRange { class VersionRange {
private static final String RANGE_REGEX = "(\\(|\\[)(.*),(.*)(\\)|\\])"
final Version lowerVersion final Version lowerVersion
final boolean lowerInclusive final boolean lowerInclusive
final Version higherVersion final Version higherVersion
final boolean higherInclusive final boolean higherInclusive
private VersionRange(Version lowerVersion, boolean lowerInclusive, protected VersionRange(Version lowerVersion, boolean lowerInclusive,
Version higherVersion, boolean higherInclusive) { Version higherVersion, boolean higherInclusive) {
this.lowerVersion = lowerVersion this.lowerVersion = lowerVersion
this.lowerInclusive = lowerInclusive this.lowerInclusive = lowerInclusive
this.higherVersion = higherVersion this.higherVersion = higherVersion
@ -86,31 +83,9 @@ class VersionRange {
sb.append("${lowerInclusive ? '>=' : '>'}${lowerVersion}") sb.append("${lowerInclusive ? '>=' : '>'}${lowerVersion}")
} }
if (higherVersion) { if (higherVersion) {
sb.append(" and ${higherInclusive ? '<=' : '<'}${higherVersion}") sb.append(" and ${higherInclusive ? '<=' : '<'}${higherVersion}")
} }
return sb.toString() return sb.toString()
} }
/**
* Parse the string representation of a {@link VersionRange}. Throws an
* {@link InvalidVersionException} if the range could not be parsed.
* @param text the range text
* @return a VersionRange instance for the specified range text
* @throws InvalidVersionException if the range text could not be parsed
*/
static VersionRange parse(String text) {
Assert.notNull(text, "Text must not be null")
def matcher = (text.trim() =~ RANGE_REGEX)
if (!matcher.matches()) {
// Try to read it as simple string
Version version = Version.parse(text)
return new VersionRange(version, true, null, true)
}
boolean lowerInclusive = matcher[0][1].equals('[')
Version lowerVersion = Version.parse(matcher[0][2])
Version higherVersion = Version.parse(matcher[0][3])
boolean higherInclusive = matcher[0][4].equals(']')
new VersionRange(lowerVersion, lowerInclusive, higherVersion, higherInclusive)
}
} }

View File

@ -112,11 +112,11 @@ class CommandLineHelpGeneratorTests {
def second = new Dependency(id: 'second', description: 'second desc', versionRange: ' [1.2.0.RELEASE,1.3.0.M1) ') def second = new Dependency(id: 'second', description: 'second desc', versionRange: ' [1.2.0.RELEASE,1.3.0.M1) ')
def metadata = InitializrMetadataTestBuilder.withDefaults().addDependencyGroup("test", first, second).build() def metadata = InitializrMetadataTestBuilder.withDefaults().addDependencyGroup("test", first, second).build()
String content = generator.generateSpringBootCliCapabilities(metadata, "https://fake-service") String content = generator.generateSpringBootCliCapabilities(metadata, "https://fake-service")
assertThat content, containsString('| first | first desc | >= 1.2.0.RELEASE |') assertThat content, containsString('| first | first desc | >=1.2.0.RELEASE |')
assertThat content, containsString('| second | second desc | [1.2.0.RELEASE,1.3.0.M1) |') assertThat content, containsString('| second | second desc | >=1.2.0.RELEASE and <1.3.0.M1 |')
} }
private assertCommandLineCapabilities(String content) { private static assertCommandLineCapabilities(String content) {
assertThat content, containsString("| Rel") assertThat content, containsString("| Rel")
assertThat content, containsString("| dependencies") assertThat content, containsString("| dependencies")
assertThat content, containsString("| applicationName") assertThat content, containsString("| applicationName")
@ -124,11 +124,11 @@ class CommandLineHelpGeneratorTests {
assertThat content, not(containsString('| Tags')) assertThat content, not(containsString('| Tags'))
} }
private static def createDependency(String id, String name) { private static createDependency(String id, String name) {
createDependency(id, name, null) createDependency(id, name, null)
} }
private static def createDependency(String id, String name, String description) { private static createDependency(String id, String name, String description) {
new Dependency(id: id, name: name, description: description) new Dependency(id: id, name: name, description: description)
} }

View File

@ -17,6 +17,7 @@
package io.spring.initializr.metadata package io.spring.initializr.metadata
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionParser
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException import org.junit.rules.ExpectedException
@ -108,4 +109,25 @@ class BillOfMaterialsTests {
bom.resolve(Version.parse('1.4.1.RELEASE')) bom.resolve(Version.parse('1.4.1.RELEASE'))
} }
@Test
void resolveRangeWithVariablePatch() {
BillOfMaterials bom = new BillOfMaterials(groupId: 'com.example',
artifactId: 'bom', version: '1.0.0')
bom.mappings << new BillOfMaterials.Mapping(
versionRange: '[1.3.0.RELEASE,1.3.x.RELEASE]', version: '1.1.0')
bom.mappings << new BillOfMaterials.Mapping(
versionRange: '[1.3.x.BUILD-SNAPSHOT,1.4.0.RELEASE)', version: '1.1.1-SNAPSHOT')
bom.validate()
bom.updateVersionRange(new VersionParser(Arrays.asList(
Version.parse("1.3.8.RELEASE"), Version.parse("1.3.9.BUILD-SNAPSHOT"))))
assertThat(bom.resolve(Version.parse('1.3.8.RELEASE')).version, equalTo('1.1.0'))
assertThat(bom.resolve(Version.parse('1.3.9.RELEASE')).version, equalTo('1.1.1-SNAPSHOT'))
bom.updateVersionRange(new VersionParser(Arrays.asList(
Version.parse("1.3.9.RELEASE"), Version.parse("1.3.10.BUILD-SNAPSHOT"))))
assertThat(bom.resolve(Version.parse('1.3.8.RELEASE')).version, equalTo('1.1.0'))
assertThat(bom.resolve(Version.parse('1.3.9.RELEASE')).version, equalTo('1.1.0'))
}
} }

View File

@ -17,6 +17,7 @@
package io.spring.initializr.metadata package io.spring.initializr.metadata
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionParser
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException import org.junit.rules.ExpectedException
@ -201,6 +202,37 @@ class DependencyTests {
'org.springframework.boot', 'spring-boot-starter-web', '0.3.0.RELEASE') // default 'org.springframework.boot', 'spring-boot-starter-web', '0.3.0.RELEASE') // default
} }
@Test
void resolveMatchingVersionWithVariablePatch() {
def dependency = new Dependency(id: 'web', description: 'A web dependency', version: '0.3.0.RELEASE',
keywords: ['foo', 'bar'], aliases: ['the-web'], facets: ['web'])
dependency.mappings << new Dependency.Mapping(
versionRange: '[1.1.0.RELEASE, 1.1.x.RELEASE]', version: '0.1.0.RELEASE')
dependency.mappings << new Dependency.Mapping(
versionRange: '[1.1.x.BUILD-SNAPSHOT, 1.2.0.RELEASE)', version: '0.2.0.RELEASE')
dependency.resolve()
dependency.updateVersionRanges(new VersionParser(Arrays.asList(
Version.parse("1.1.5.RELEASE"), Version.parse("1.1.6.BUILD-SNAPSHOT"))))
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.5.RELEASE')),
'org.springframework.boot', 'spring-boot-starter-web', '0.1.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.6.BUILD-SNAPSHOT')),
'org.springframework.boot', 'spring-boot-starter-web', '0.2.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('2.1.3.M1')),
'org.springframework.boot', 'spring-boot-starter-web', '0.3.0.RELEASE') // default
dependency.updateVersionRanges(new VersionParser(Arrays.asList(
Version.parse("1.1.6.RELEASE"), Version.parse("1.1.7.BUILD-SNAPSHOT"))))
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.5.RELEASE')),
'org.springframework.boot', 'spring-boot-starter-web', '0.1.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.6.RELEASE')),
'org.springframework.boot', 'spring-boot-starter-web', '0.1.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('1.1.7.BUILD-SNAPSHOT')),
'org.springframework.boot', 'spring-boot-starter-web', '0.2.0.RELEASE')
validateResolvedWebDependency(dependency.resolve(Version.parse('2.1.3.M1')),
'org.springframework.boot', 'spring-boot-starter-web', '0.3.0.RELEASE') // default
}
static void validateResolvedWebDependency( static void validateResolvedWebDependency(
def dependency, def expectedGroupId, def expectedArtifactId, def expectedVersion) { def dependency, def expectedGroupId, def expectedArtifactId, def expectedVersion) {
assertEquals expectedVersion, dependency.version assertEquals expectedVersion, dependency.version

View File

@ -17,10 +17,13 @@
package io.spring.initializr.metadata package io.spring.initializr.metadata
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
import io.spring.initializr.util.Version
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException import org.junit.rules.ExpectedException
import static org.assertj.core.api.Assertions.assertThat
/** /**
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
@ -146,6 +149,34 @@ class InitializrMetadataTests {
builder.build() builder.build()
} }
@Test
void updateSpringBootVersions() {
def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.3.0.RELEASE,1.3.x.RELEASE]', version: '1.0.0')
bom.mappings << new BillOfMaterials.Mapping(versionRange: '1.3.x.BUILD-SNAPSHOT', version: '1.1.0-BUILD-SNAPSHOT')
def dependency = new Dependency(id: 'bar')
dependency.mappings << new Dependency.Mapping(
versionRange: '[1.3.0.RELEASE, 1.3.x.RELEASE]', version: '0.1.0.RELEASE')
dependency.mappings << new Dependency.Mapping(
versionRange: '1.3.x.BUILD-SNAPSHOT', version: '0.2.0.RELEASE')
InitializrMetadata metadata = InitializrMetadataTestBuilder
.withDefaults().addDependencyGroup("test", dependency)
.addBom('foo-bom', bom).build();
List<DefaultMetadataElement> bootVersions = Arrays.asList(
new DefaultMetadataElement(id: '1.3.6.RELEASE', name: '1.3.6'),
new DefaultMetadataElement(id: '1.3.7.BUILD-SNAPSHOT', name: '1.3.7'))
metadata.updateSpringBootVersions(bootVersions)
assertThat(metadata.configuration.env.boms['foo-bom']
.resolve(Version.parse('1.3.6.RELEASE')).version).isEqualTo('1.0.0')
assertThat(metadata.configuration.env.boms['foo-bom']
.resolve(Version.parse('1.3.7.BUILD-SNAPSHOT')).version).isEqualTo('1.1.0-BUILD-SNAPSHOT')
assertThat(metadata.dependencies.get('bar')
.resolve(Version.parse('1.3.6.RELEASE')).version).isEqualTo('0.1.0.RELEASE')
assertThat(metadata.dependencies.get('bar')
.resolve(Version.parse('1.3.7.BUILD-SNAPSHOT')).version).isEqualTo('0.2.0.RELEASE')
}
@Test @Test
void invalidParentMissingVersion() { void invalidParentMissingVersion() {
InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder

View File

@ -0,0 +1,134 @@
/*
* 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.util
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import static org.hamcrest.MatcherAssert.assertThat
import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.lessThan
import static org.junit.Assert.assertNull
/**
* Tests for {@link VersionParser}.
*
* @author Stephane Nicoll
*/
class VersionParserTests {
@Rule
public final ExpectedException thrown = ExpectedException.none()
private VersionParser parser = new VersionParser(Collections.EMPTY_LIST)
@Test
void noQualifierString() {
def version = parser.parse('1.2.0')
assertThat(version.toString(), equalTo('1.2.0'))
}
@Test
void withQualifierString() {
def version = parser.parse('1.2.0.RELEASE')
assertThat(version.toString(), equalTo('1.2.0.RELEASE'))
}
@Test
void withQualifierAndVersionString() {
def version = parser.parse('1.2.0.RC2')
assertThat(version.toString(), equalTo('1.2.0.RC2'))
}
@Test
void parseInvalidVersion() {
thrown.expect(InvalidVersionException)
parser.parse('foo')
}
@Test
void safeParseInvalidVersion() {
assertNull parser.safeParse('foo')
}
@Test
void parseVersionWithSpaces() {
assertThat(parser.parse(' 1.2.0.RC3 '),
lessThan(parser.parse('1.3.0.RELEASE')))
}
@Test
void parseVariableVersionMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.3.9.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('1.3.x.BUILD-SNAPSHOT').toString(),
equalTo('1.3.9.BUILD-SNAPSHOT'))
}
@Test
void parseVariableVersionNoPatchMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.3.9.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('1.x.x.RELEASE').toString(),
equalTo('1.3.8.RELEASE'))
}
@Test
void parseVariableVersionNoQualifierMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.4.0.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('1.4.x').toString(),
equalTo('1.4.0.BUILD-SNAPSHOT'))
}
@Test
void parseVariableVersionNoMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.3.9.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('1.4.x.BUILD-SNAPSHOT').toString(),
equalTo("1.4.999.BUILD-SNAPSHOT"))
}
@Test
void parseVariableVersionNoPatchNoMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.3.9.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('2.x.x.RELEASE').toString(),
equalTo("2.999.999.RELEASE"))
}
@Test
void parseVariableVersionNoQualifierNoMatch() {
List<Version> currentVersions = Arrays.asList(parser.parse('1.3.8.RELEASE'),
parser.parse('1.4.0.BUILD-SNAPSHOT'))
parser = new VersionParser(currentVersions)
assertThat(parser.parse('1.2.x').toString(), equalTo("1.2.999"))
}
@Test
void invalidRange() {
thrown.expect(InvalidVersionException)
parser.parseRange("foo-bar")
}
}

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -83,29 +83,54 @@ class VersionRangeTests {
assertThat('1.1.9.RELEASE', not(match('1.2.0.RELEASE'))) assertThat('1.1.9.RELEASE', not(match('1.2.0.RELEASE')))
} }
@Test
void invalidRange() {
thrown.expect(InvalidVersionException)
VersionRange.parse("foo-bar")
}
@Test @Test
void rangeWithSpaces() { void rangeWithSpaces() {
assertThat('1.2.0.RC3', match('[ 1.2.0.RC1 , 1.2.0.RC5]')) assertThat('1.2.0.RC3', match('[ 1.2.0.RC1 , 1.2.0.RC5]'))
} }
@Test
void matchLatestVersion() {
assertThat('1.2.8.RELEASE', match('[1.2.0.RELEASE,1.2.x.BUILD-SNAPSHOT]',
new VersionParser(Arrays.asList(Version.parse('1.2.9.BUILD-SNAPSHOT')))))
}
@Test
void matchOverLatestVersion() {
assertThat('1.2.10.RELEASE', not(match('[1.2.0.RELEASE,1.2.x.BUILD-SNAPSHOT]',
new VersionParser(Arrays.asList(Version.parse('1.2.9.BUILD-SNAPSHOT'))))))
}
@Test
void matchAsOfCurrentVersion() {
assertThat('1.3.5.RELEASE', match('[1.3.x.RELEASE,1.3.x.BUILD-SNAPSHOT]',
new VersionParser(Arrays.asList(Version.parse('1.3.4.RELEASE'),
Version.parse('1.3.6.BUILD-SNAPSHOT')))))
}
@Test
void matchOverAsOfCurrentVersion() {
assertThat('1.3.5.RELEASE', not(match('[1.3.x.RELEASE,1.3.x.BUILD-SNAPSHOT]',
new VersionParser(Arrays.asList(Version.parse('1.3.7.RELEASE'),
Version.parse('1.3.6.BUILD-SNAPSHOT'))))))
}
private static VersionRangeMatcher match(String range) { private static VersionRangeMatcher match(String range) {
new VersionRangeMatcher(range) new VersionRangeMatcher(range, new VersionParser(Collections.EMPTY_LIST))
}
private static VersionRangeMatcher match(String range, VersionParser parser) {
new VersionRangeMatcher(range, parser)
} }
static class VersionRangeMatcher extends BaseMatcher<String> { static class VersionRangeMatcher extends BaseMatcher<String> {
private final VersionRange range; private final VersionRange range;
private final VersionParser parser;
VersionRangeMatcher(String text) { VersionRangeMatcher(String text, VersionParser parser) {
this.range = VersionRange.parse(text) this.parser = parser
this.range = parser.parseRange(text)
} }
@Override @Override
@ -113,12 +138,13 @@ class VersionRangeTests {
if (!item instanceof String) { if (!item instanceof String) {
return false; return false;
} }
return this.range.match(Version.parse(item)) return this.range.match(this.parser.parse((String) item))
} }
@Override @Override
void describeTo(Description description) { void describeTo(Description description) {
description.appendText(range) description.appendText(range.toString())
} }
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,41 +16,20 @@
package io.spring.initializr.util package io.spring.initializr.util
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException
import static io.spring.initializr.util.Version.parse
import static io.spring.initializr.util.Version.safeParse
import static org.hamcrest.MatcherAssert.assertThat import static org.hamcrest.MatcherAssert.assertThat
import static org.hamcrest.Matchers.* import static org.hamcrest.Matchers.comparesEqualTo
import static org.junit.Assert.assertNull import static org.hamcrest.Matchers.equalTo
import static org.hamcrest.Matchers.greaterThan
import static org.hamcrest.Matchers.lessThan
/** /**
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
class VersionTests { class VersionTests {
@Rule private static final VersionParser parser = new VersionParser(Collections.EMPTY_LIST)
public final ExpectedException thrown = ExpectedException.none()
@Test
void noQualifierString() {
def version = parse('1.2.0')
assertThat(version.toString(), equalTo('1.2.0'))
}
@Test
void withQualifierString() {
def version = parse('1.2.0.RELEASE')
assertThat(version.toString(), equalTo('1.2.0.RELEASE'))
}
@Test
void withQualifierAndVersionString() {
def version = parse('1.2.0.RC2')
assertThat(version.toString(), equalTo('1.2.0.RC2'))
}
@Test @Test
void equalNoQualifier() { void equalNoQualifier() {
@ -146,20 +125,9 @@ class VersionTests {
assertThat(parse('1.2.0.BUILD-SNAPSHOT'), lessThan(parse('1.2.0.RELEASE'))) assertThat(parse('1.2.0.BUILD-SNAPSHOT'), lessThan(parse('1.2.0.RELEASE')))
} }
@Test private static Version parse(String text) {
void parseInvalidVersion() { def version = parser.parse(text)
thrown.expect(InvalidVersionException) version
parse('foo')
}
@Test
void safeParseInvalidVersion() {
assertNull safeParse('foo')
}
@Test
void parseVersionWithSpaces() {
assertThat(parse(' 1.2.0.RC3 '), lessThan(parse('1.3.0.RELEASE')))
} }
} }

View File

@ -55,8 +55,7 @@ class DefaultInitializrMetadataProvider implements InitializrMetadataProvider {
if (!bootVersions.find { it.default }) { // No default specified if (!bootVersions.find { it.default }) { // No default specified
bootVersions[0].default = true bootVersions[0].default = true
} }
metadata.bootVersions.content.clear() metadata.updateSpringBootVersions(bootVersions)
metadata.bootVersions.content.addAll(bootVersions)
} }
} }

View File

@ -22,7 +22,6 @@ import groovy.json.JsonBuilder
import io.spring.initializr.metadata.Dependency import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadataProvider import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.util.Version import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionRange
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType import org.springframework.http.MediaType
@ -52,7 +51,7 @@ class UiController {
dependencyGroups.each { g -> dependencyGroups.each { g ->
g.content.each { d -> g.content.each { d ->
if (v && d.versionRange) { if (v && d.versionRange) {
if (VersionRange.parse(d.versionRange).match(v)) { if (d.match(v)) {
content << new DependencyItem(g.name, d) content << new DependencyItem(g.name, d)
} }
} else { } else {