diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 94c01d5e..6c714fa5 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,6 +7,7 @@ order. === Release 1.0.0 (In progress) +* https://github.com/spring-io/initializr/issues/62[#62]: add version range support. * https://github.com/spring-io/initializr/issues/75[#75]: migrate smoke tests to Geb. * https://github.com/spring-io/initializr/issues/74[#74]: remove support for meta-data V1. * https://github.com/spring-io/initializr/issues/71[#71]: update project layout for Groovy-based projects. diff --git a/initializr-service/application.yml b/initializr-service/application.yml index 202b1b64..982dc052 100644 --- a/initializr-service/application.yml +++ b/initializr-service/application.yml @@ -4,7 +4,7 @@ info: version: 0.3.1 # remember to update static/install.sh as well: spring-boot: - version: 1.2.0.RELEASE + version: 1.2.1.RELEASE initializr: dependencies: @@ -16,6 +16,14 @@ initializr: - name: AOP id: aop description: Support for aspect-oriented programming including spring-aop and AspectJ + - name: Atomikos (JTA) + id: jta-atomikos + description: Support for JTA distributed transactions via Atomikos + versionRange: 1.2.0.M1 + - name: Bitronix (JTA) + id: jta-bitronix + description: Support for JTA distributed transactions via Bitronix + versionRange: 1.2.0.M1 - name: Data content: - name: JDBC @@ -55,6 +63,10 @@ initializr: - name: AMQP id: amqp description: Support for the Advanced Message Queuing Protocol via spring-rabbit + - name: Mail + id: mail + description: Support for javax.mail + versionRange: 1.2.0.RC1 - name: Web content: - name: Web @@ -68,9 +80,17 @@ initializr: - name: WS id: ws description: Support for Spring Web Services + - name: Jersey (JAX-RS) + id: jersey + description: Support for the Jersey RESTful Web Services framework + versionRange: 1.2.0.RELEASE - name: Rest Repositories id: data-rest description: Support for exposing Spring Data repositories over REST via spring-data-rest-webmvc + - name: HATEOAS + id: hateoas + description: Support for HATEOAS-based RESTful services + versionRange: 1.2.2.BUILD-SNAPSHOT - name: Mobile id: mobile description: Support for spring-mobile @@ -96,6 +116,12 @@ initializr: description: Support for the Thymeleaf templating engine, including integration with Spring facets: - web + - name: Mustache + id: mustache + description: Support for the Mustache templating engine + versionRange: 1.2.2.BUILD-SNAPSHOT + facets: + - web - name: Social content: - name: Facebook @@ -112,6 +138,10 @@ initializr: - name: Actuator id: actuator description: Production ready features to help you monitor and manage your application + - name: Cloud Connectors + id: cloud-connectors + description: Simplifies connecting to services in cloud platforms + versionRange: 1.2.0.RELEASE - name: Remote Shell id: remote-shell description: Support for CRaSH diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy index ec7b099e..1101dcb4 100644 --- a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy @@ -20,6 +20,11 @@ import javax.annotation.PostConstruct import groovy.transform.ToString import groovy.util.logging.Slf4j +import io.spring.initializr.mapper.InitializrMetadataJsonMapper +import io.spring.initializr.mapper.InitializrMetadataV21JsonMapper +import io.spring.initializr.mapper.InitializrMetadataV2JsonMapper +import io.spring.initializr.support.InvalidVersionException +import io.spring.initializr.support.VersionRange import org.springframework.boot.context.properties.ConfigurationProperties @@ -61,8 +66,6 @@ class InitializrMetadata { private final Map indexedDependencies = [:] - private final transient InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataJsonMapper() - /** * Return the {@link Dependency} with the specified id or {@code null} if * no such dependency exists. @@ -127,11 +130,11 @@ class InitializrMetadata { /** * Generate a JSON representation of the current metadata - * + * @param version the meta-data version * @param appUrl the application url */ - String generateJson(String appUrl) { - jsonMapper.write(this, appUrl) + String generateJson(InitializrMetadataVersion version, String appUrl) { + getJsonMapper(version).write(this, appUrl) } /** @@ -193,6 +196,14 @@ class InitializrMetadata { "Invalid dependency, id should have the form groupId:artifactId[:version] but got $id") } } + if (dependency.versionRange) { + try { + VersionRange.parse(dependency.versionRange) + } catch (InvalidVersionException ex) { + throw new InvalidInitializrMetadataException("Invalid version range '$dependency.versionRange' for " + + "dependency with id '$dependency.id'") + } + } } static def getDefault(List elements) { @@ -205,6 +216,13 @@ class InitializrMetadata { return (elements.isEmpty() ? null : elements.get(0).id) } + private static InitializrMetadataJsonMapper getJsonMapper(InitializrMetadataVersion version) { + switch(version) { + case InitializrMetadataVersion.V2: return new InitializrMetadataV2JsonMapper(); + default: return new InitializrMetadataV21JsonMapper(); + } + } + static class DependencyGroup { String name @@ -228,6 +246,8 @@ class InitializrMetadata { String description + String versionRange + /** * Specify if the dependency has its coordinates set, i.e. {@code groupId} * and {@code artifactId}. diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataVersion.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataVersion.groovy new file mode 100644 index 00000000..0ec58f5e --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataVersion.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2015 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 + +import org.springframework.http.MediaType + +/** + * Define the supported meta-data version. + * + * @author Stephane Nicoll + * @since 1.0 + */ +enum InitializrMetadataVersion { + + /** + * HAL-compliant metadata. + */ + V2('application/vnd.initializr.v2+json'), + + /** + * Add 'versionRange' attribute to any dependency to specify which + * Spring Boot versions are compatible with it. + */ + V2_1('application/vnd.initializr.v2.1+json') + + private final MediaType mediaType; + + InitializrMetadataVersion(String mediaType) { + this.mediaType = MediaType.parseMediaType(mediaType) + } + + MediaType getMediaType() { + return mediaType + } + +} \ No newline at end of file diff --git a/initializr/src/main/groovy/io/spring/initializr/ProjectRequest.groovy b/initializr/src/main/groovy/io/spring/initializr/ProjectRequest.groovy index 19b80bda..737b5321 100644 --- a/initializr/src/main/groovy/io/spring/initializr/ProjectRequest.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/ProjectRequest.groovy @@ -17,6 +17,8 @@ package io.spring.initializr import groovy.util.logging.Slf4j +import io.spring.initializr.support.Version +import io.spring.initializr.support.VersionRange /** * A request to generate a project. @@ -79,12 +81,21 @@ class ProjectRequest { } dependency } + String actualBootVersion = bootVersion ?: metadata.defaults.bootVersion + Version requestedVersion = Version.parse(actualBootVersion) resolvedDependencies.each { it.facets.each { if (!facets.contains(it)) { facets.add(it) } } + if (it.versionRange) { + def range = VersionRange.parse(it.versionRange) + if (!range.match(requestedVersion)) { + throw new InvalidProjectRequestException("Dependency '$it.id' is not compatible " + + "with Spring Boot $bootVersion") + } + } } if (this.type) { diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapper.groovy new file mode 100644 index 00000000..280c46f7 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapper.groovy @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2015 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.mapper + +import io.spring.initializr.InitializrMetadata + +/** + * Generate a JSON representation of the metadata. + * + * @author Stephane Nicoll + * @since 1.0 + */ +interface InitializrMetadataJsonMapper { + + /** + * Write a json representation of the specified meta-data. + */ + String write(InitializrMetadata metadata, String appUrl); + +} \ No newline at end of file diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy new file mode 100644 index 00000000..e8c11587 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2015 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.mapper + +/** + * A {@link InitializrMetadataJsonMapper} handling the meta-data format for v2.1 + *

+ * Version 2.1 brings the 'versionRange' attribute for a dependency to restrict + * the Spring Boot versions that can be used against it. + * + * @author Stephane Nicoll + * @since 1.0 + */ +class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMapper { + + @Override + protected mapDependency(dependency) { + def content = mapValue(dependency) + if (dependency.versionRange) { + content['versionRange'] = dependency.versionRange + } + content + } +} diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV2JsonMapper.groovy similarity index 80% rename from initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy rename to initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV2JsonMapper.groovy index 68e5f579..93ee7e5b 100644 --- a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadataJsonMapper.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV2JsonMapper.groovy @@ -14,25 +14,27 @@ * limitations under the License. */ -package io.spring.initializr +package io.spring.initializr.mapper import groovy.json.JsonBuilder +import io.spring.initializr.InitializrMetadata import org.springframework.hateoas.TemplateVariable import org.springframework.hateoas.TemplateVariables import org.springframework.hateoas.UriTemplate +import org.springframework.util.StringUtils /** - * Generate a JSON representation of the metadata. + * A {@link InitializrMetadataJsonMapper} handling the meta-data format for v2. * * @author Stephane Nicoll * @since 1.0 */ -class InitializrMetadataJsonMapper { +class InitializrMetadataV2JsonMapper implements InitializrMetadataJsonMapper { private final TemplateVariables templateVariables - InitializrMetadataJsonMapper() { + InitializrMetadataV2JsonMapper() { this.templateVariables = new TemplateVariables( new TemplateVariable('dependencies', TemplateVariable.VariableType.REQUEST_PARAM), new TemplateVariable('packaging', TemplateVariable.VariableType.REQUEST_PARAM), @@ -48,6 +50,7 @@ class InitializrMetadataJsonMapper { ) } + @Override String write(InitializrMetadata metadata, String appUrl) { JsonBuilder json = new JsonBuilder() json { @@ -68,7 +71,7 @@ class InitializrMetadataJsonMapper { json.toString() } - private links(parent, types, appUrl) { + protected links(parent, types, appUrl) { def content = [:] types.each { content[it.id] = link(appUrl, it) @@ -76,7 +79,7 @@ class InitializrMetadataJsonMapper { parent._links content } - private link(appUrl, type) { + protected link(appUrl, type) { def result = [:] result.href = generateTemplatedUri(appUrl, type) result.templated = true @@ -91,41 +94,40 @@ class InitializrMetadataJsonMapper { } - private static dependencies(parent, groups) { + protected dependencies(parent, groups) { parent.dependencies { type 'hierarchical-multi-select' values groups.collect { - processDependencyGroup(it) + mapDependencyGroup(it) } } - } - private static type(parent, defaultValue, dependencies) { + protected type(parent, defaultValue, dependencies) { parent.type { type 'action' if (defaultValue) { 'default' defaultValue } values dependencies.collect { - processType(it) + mapType(it) } } } - private static singleSelect(parent, name, defaultValue, itemValues) { + protected singleSelect(parent, name, defaultValue, itemValues) { parent."$name" { type 'single-select' if (defaultValue) { 'default' defaultValue } values itemValues.collect { - processValue(it) + mapValue(it) } } } - private static text(parent, name, value) { + protected text(parent, name, value) { parent."$name" { type 'text' if (value) { @@ -134,8 +136,7 @@ class InitializrMetadataJsonMapper { } } - - private static processDependencyGroup(group) { + protected mapDependencyGroup(group) { def result = [:] result.name = group.name if (group.hasProperty('description') && group.description) { @@ -143,20 +144,29 @@ class InitializrMetadataJsonMapper { } def items = [] group.content.collect { - items << processValue(it) + def dependency = mapDependency(it) + if (dependency) { + items << dependency + } } result.values = items result } - private static processType(type) { - def result = processValue(type) + protected mapDependency(dependency) { + if (!dependency.versionRange) { // only map the dependency if no versionRange is set + mapValue(dependency) + } + } + + protected mapType(type) { + def result = mapValue(type) result.action = type.action result.tags = type.tags result } - private static processValue(value) { + protected mapValue(value) { def result = [:] result.id = value.id result.name = value.name diff --git a/initializr/src/main/groovy/io/spring/initializr/support/InvalidVersionException.groovy b/initializr/src/main/groovy/io/spring/initializr/support/InvalidVersionException.groovy new file mode 100644 index 00000000..8ec74daa --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/support/InvalidVersionException.groovy @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2015 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.support + +import groovy.transform.InheritConstructors + +/** + * Thrown if a input represents an invalid version. + * + * @author Stephane Nicoll + */ +@InheritConstructors +class InvalidVersionException extends RuntimeException { + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/support/Version.groovy b/initializr/src/main/groovy/io/spring/initializr/support/Version.groovy index 082e6684..99e16423 100644 --- a/initializr/src/main/groovy/io/spring/initializr/support/Version.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/support/Version.groovy @@ -19,6 +19,8 @@ package io.spring.initializr.support import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.springframework.util.Assert + /** * Define the version number of a module. A typical version is represented * as {@code MAJOR.MINOR.PATCH.QUALIFER} where the qualifier can have an @@ -49,16 +51,18 @@ class Version implements Comparable { /** * Parse the string representation of a {@link Version}. Throws an - * {@link IllegalArgumentException} if the version could not be parsed. + * {@link InvalidVersionException} if the version could not be parsed. * @param text the version text * @return a Version instance for the specified version text - * @throws IllegalArgumentException if the version text could not be parsed + * @throws InvalidVersionException if the version text could not be parsed * @see #safeParse(java.lang.String) */ static Version parse(String text) { - def matcher = (text =~ VERSION_REGEX) + Assert.notNull(text, 'Text must not be null') + def matcher = (text.trim() =~ VERSION_REGEX) if (!matcher.matches()) { - throw new IllegalArgumentException("Could not determine version based on $text") + 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]) @@ -87,7 +91,7 @@ class Version implements Comparable { static safeParse(String text) { try { return parse(text) - } catch (IllegalArgumentException e) { + } catch (InvalidVersionException e) { return null } } diff --git a/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy b/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy new file mode 100644 index 00000000..de5a4097 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2015 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.support + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +import org.springframework.util.Assert + +/** + * Define a {@link Version} range. A square bracket "[" or "]" denotes an inclusive + * end of the range and a round bracket "(" or ")" denotes an exclusive end of the + * range. A range can also be unbounded by defining a a single {@link Version}. The + * examples below make this clear. + * + *

+ * + * @author Stephane Nicoll + * @since 1.0 + */ +@ToString +@EqualsAndHashCode +class VersionRange { + + private static final String RANGE_REGEX = "(\\(|\\[)(.*),(.*)(\\)|\\])" + + private Version lowerVersion + private boolean lowerInclusive + private Version higherVersion + private boolean higherInclusive + + /** + * Specify if the {@link Version} matches this range. Returns {@code true} + * if the version is contained within this range, {@code false} otherwise. + */ + boolean match(Version version) { + Assert.notNull(version, "Version must not be null") + def lower = lowerVersion.compareTo(version) + if (lower > 0) { + return false; + } else if (!lowerInclusive && lower == 0) { + return false; + } + if (higherVersion) { + def higher = higherVersion.compareTo(version) + if (higher < 0) { + return false + } else if (!higherInclusive && higher == 0) { + return false + } + } + return true + } + + /** + * 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(lowerInclusive: true, lowerVersion: version) + } + VersionRange range = new VersionRange() + range.lowerInclusive = matcher[0][1].equals('[') + range.lowerVersion = Version.parse(matcher[0][2]) + range.higherVersion = Version.parse(matcher[0][3]) + range.higherInclusive = matcher[0][4].equals(']') + range + } + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy index d6b5aff8..37b42a33 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy @@ -18,6 +18,7 @@ package io.spring.initializr.web import groovy.util.logging.Slf4j import io.spring.initializr.CommandLineHelpGenerator +import io.spring.initializr.InitializrMetadataVersion import io.spring.initializr.ProjectGenerator import io.spring.initializr.ProjectRequest @@ -79,11 +80,20 @@ class MainController extends AbstractInitializrController { builder.body(commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl)) } - @RequestMapping(value = "/", produces = ["application/vnd.initializr.v2+json", "application/json"]) - ResponseEntity serviceCapabilities() { + @RequestMapping(value = "/", produces = ["application/vnd.initializr.v2.1+json", "application/json"]) + ResponseEntity serviceCapabilitiesV21() { + serviceCapabilitiesFor(InitializrMetadataVersion.V2_1) + } + + @RequestMapping(value = "/", produces = ["application/vnd.initializr.v2+json"]) + ResponseEntity serviceCapabilitiesV2() { + serviceCapabilitiesFor(InitializrMetadataVersion.V2) + } + + private ResponseEntity serviceCapabilitiesFor(InitializrMetadataVersion version) { String appUrl = ServletUriComponentsBuilder.fromCurrentServletMapping().build() - def content = metadataProvider.get().generateJson(appUrl) - return ResponseEntity.ok().contentType(META_DATA_V2).body(content) + def content = metadataProvider.get().generateJson(version, appUrl) + return ResponseEntity.ok().contentType(version.mediaType).body(content) } @RequestMapping(value = '/', produces = 'text/html') diff --git a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy index 3b266621..932d8472 100644 --- a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy @@ -97,6 +97,16 @@ class InitializrMetadataTests { metadata.validateDependency(new InitializrMetadata.Dependency()) } + @Test + void invalidSpringBootRange() { + def dependency = createDependency('web') + dependency.versionRange = 'A.B.C' + + thrown.expect(InvalidInitializrMetadataException) + thrown.expectMessage('A.B.C') + metadata.validateDependency(dependency) + } + @Test void invalidIdFormatTooManyColons() { def dependency = createDependency('org.foo:bar:1.0:test:external') diff --git a/initializr/src/test/groovy/io/spring/initializr/ProjectRequestTests.groovy b/initializr/src/test/groovy/io/spring/initializr/ProjectRequestTests.groovy index 22d50736..8762f839 100644 --- a/initializr/src/test/groovy/io/spring/initializr/ProjectRequestTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/ProjectRequestTests.groovy @@ -95,6 +95,37 @@ class ProjectRequestTests { thrown.expect(InvalidProjectRequestException) thrown.expectMessage('org.foo:acme') request.resolve(metadata) + assertEquals(1, request.resolvedDependencies.size()) + } + + @Test + void resolveDependencyInRange() { + def request = new ProjectRequest() + def dependency = createDependency('org.foo', 'bar', '1.2.0.RELEASE') + dependency.versionRange = '[1.0.1.RELEASE, 1.2.0.RELEASE)' + def metadata = InitializrMetadataBuilder.withDefaults() + .addDependencyGroup('code', dependency).validateAndGet() + + request.style << 'org.foo:bar' + request.bootVersion = '1.1.2.RELEASE' + request.resolve(metadata) + } + + @Test + void resolveDependencyNotInRange() { + def request = new ProjectRequest() + def dependency = createDependency('org.foo', 'bar', '1.2.0.RELEASE') + dependency.versionRange = '[1.0.1.RELEASE, 1.2.0.RELEASE)' + def metadata = InitializrMetadataBuilder.withDefaults() + .addDependencyGroup('code', dependency).validateAndGet() + + request.style << 'org.foo:bar' + request.bootVersion = '0.9.9.RELEASE' + + thrown.expect(InvalidProjectRequestException) + thrown.expectMessage('org.foo:bar') + thrown.expectMessage('0.9.9.RELEASE') + request.resolve(metadata) } @Test diff --git a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy b/initializr/src/test/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapperTests.groovy similarity index 94% rename from initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy rename to initializr/src/test/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapperTests.groovy index 94f034ae..9e154c6d 100644 --- a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataJsonMapperTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/mapper/InitializrMetadataJsonMapperTests.groovy @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.spring.initializr +package io.spring.initializr.mapper import groovy.json.JsonSlurper +import io.spring.initializr.InitializrMetadata import io.spring.initializr.test.InitializrMetadataBuilder import org.junit.Test @@ -27,7 +28,7 @@ import static org.junit.Assert.assertEquals */ class InitializrMetadataJsonMapperTests { - private final InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataJsonMapper() + private final InitializrMetadataJsonMapper jsonMapper = new InitializrMetadataV21JsonMapper() private final JsonSlurper slurper = new JsonSlurper() @Test diff --git a/initializr/src/test/groovy/io/spring/initializr/support/VersionRangeTests.groovy b/initializr/src/test/groovy/io/spring/initializr/support/VersionRangeTests.groovy new file mode 100644 index 00000000..248b6c6d --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/support/VersionRangeTests.groovy @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2015 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.support + +import org.hamcrest.BaseMatcher +import org.hamcrest.Description +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + +import static org.hamcrest.core.IsNot.not +import static org.junit.Assert.assertThat + +/** + * @author Stephane Nicoll + */ +class VersionRangeTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none() + + @Test + void matchSimpleRange() { + assertThat('1.2.0.RC3', match('[1.2.0.RC1,1.2.0.RC5]')) + } + + @Test + void matchSimpleRangeBefore() { + assertThat('1.1.9.RC3', not(match('[1.2.0.RC1,1.2.0.RC5]'))) + } + + @Test + void matchSimpleRangeAfter() { + assertThat('1.2.0.RC6', not(match('[1.2.0.RC1,1.2.0.RC5]'))) + } + + @Test + void matchInclusiveLowerRange() { + assertThat('1.2.0.RC1', match('[1.2.0.RC1,1.2.0.RC5]')) + } + + @Test + void matchInclusiveHigherRange() { + assertThat('1.2.0.RC5', match('[1.2.0.RC1,1.2.0.RC5]')) + } + + @Test + void matchExclusiveLowerRange() { + assertThat('1.2.0.RC1', not(match('(1.2.0.RC1,1.2.0.RC5)'))) + } + + @Test + void matchExclusiveHigherRange() { + assertThat('1.2.0.RC5', not(match('[1.2.0.RC1,1.2.0.RC5)'))) + } + + @Test + void matchUnboundedRangeEqual() { + assertThat('1.2.0.RELEASE', match('1.2.0.RELEASE')) + } + + @Test + void matchUnboundedRangeAfter() { + assertThat('2.2.0.RELEASE', match('1.2.0.RELEASE')) + } + + @Test + void matchUnboundedRangeBefore() { + assertThat('1.1.9.RELEASE', not(match('1.2.0.RELEASE'))) + } + + @Test + void invalidRange() { + thrown.expect(InvalidVersionException) + VersionRange.parse("foo-bar") + } + + @Test + void rangeWithSpaces() { + assertThat('1.2.0.RC3', match('[ 1.2.0.RC1 , 1.2.0.RC5]')) + } + + + private static VersionRangeMatcher match(String range) { + new VersionRangeMatcher(range) + } + + + static class VersionRangeMatcher extends BaseMatcher { + + private final VersionRange range; + + VersionRangeMatcher(String text) { + this.range = VersionRange.parse(text) + } + + @Override + boolean matches(Object item) { + if (!item instanceof String) { + return false; + } + return this.range.match(Version.parse(item)) + } + + @Override + void describeTo(Description description) { + description.appendText(range) + } + } +} diff --git a/initializr/src/test/groovy/io/spring/initializr/support/VersionTests.groovy b/initializr/src/test/groovy/io/spring/initializr/support/VersionTests.groovy index 49243222..b178d93b 100644 --- a/initializr/src/test/groovy/io/spring/initializr/support/VersionTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/support/VersionTests.groovy @@ -130,7 +130,7 @@ class VersionTests { @Test void parseInvalidVersion() { - thrown.expect(IllegalArgumentException) + thrown.expect(InvalidVersionException) parse('foo') } @@ -139,4 +139,9 @@ class VersionTests { assertNull safeParse('foo') } + @Test + void parseVersionWithSpaces() { + assertThat(parse(' 1.2.0.RC3 '), lessThan(parse('1.3.0.RELEASE'))) + } + } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy index eb404240..78f709dd 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy @@ -19,7 +19,9 @@ package io.spring.initializr.web import java.nio.charset.Charset import groovy.json.JsonSlurper +import io.spring.initializr.InitializrMetadataVersion import org.json.JSONObject +import org.junit.Ignore import org.junit.Test import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode @@ -43,8 +45,7 @@ import static org.junit.Assert.* @ActiveProfiles('test-default') class MainControllerIntegrationTests extends AbstractInitializrControllerIntegrationTests { - private static final MediaType CURRENT_METADATA_MEDIA_TYPE = - MediaType.parseMediaType('application/vnd.initializr.v2+json') + private static final MediaType CURRENT_METADATA_MEDIA_TYPE = InitializrMetadataVersion.V2_1.mediaType private final def slurper = new JsonSlurper() @@ -60,10 +61,27 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra @Test void simpleTgzProject() { - downloadTgz('/starter.tgz?style=org.acme:bar').isJavaProject().isMavenProject() + downloadTgz('/starter.tgz?style=org.acme:foo').isJavaProject().isMavenProject() .hasStaticAndTemplatesResources(false).pomAssert() .hasDependenciesCount(2) - .hasDependency('org.acme', 'bar', '2.1.0') + .hasDependency('org.acme', 'foo', '1.3.5') + } + + @Test + void dependencyInRange() { + downloadTgz('/starter.tgz?style=org.acme:biz&bootVersion=1.2.1.RELEASE').isJavaProject().isMavenProject() + .hasStaticAndTemplatesResources(false).pomAssert() + .hasDependenciesCount(2) + .hasDependency('org.acme', 'biz', '1.3.5') + } + + @Test + void dependencyNotInRange() { + try { + execute('/starter.tgz?style=org.acme:bur', byte[], null, null) + } catch (HttpClientErrorException ex) { + assertEquals HttpStatus.NOT_ACCEPTABLE, ex.statusCode + } } @Test @@ -136,12 +154,34 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra } @Test - void metadataWithCurrentAcceptHeader() { + @Ignore("Need a comparator that does not care about the number of elements in an array") + void currentMetadataCompatibleWithV2() { + ResponseEntity response = invokeHome(null, '*/*') + validateMetadata(response, CURRENT_METADATA_MEDIA_TYPE, '2.0.0', JSONCompareMode.LENIENT) + } + + @Test + void metadataWithV2AcceptHeader() { ResponseEntity response = invokeHome(null, 'application/vnd.initializr.v2+json') + validateMetadata(response, InitializrMetadataVersion.V2.mediaType, '2.0.0', JSONCompareMode.STRICT) + } + + @Test + void metadataWithCurrentAcceptHeader() { + ResponseEntity response = invokeHome(null, 'application/vnd.initializr.v2.1+json') validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) validateCurrentMetadata(new JSONObject(response.body)) } + @Test + void metadataWithUnknownAcceptHeader() { + try { + invokeHome(null, 'application/vnd.initializr.v5.4+json') + } catch (HttpClientErrorException ex) { + assertEquals HttpStatus.NOT_ACCEPTABLE, ex.statusCode + } + } + @Test void curlReceivesTextByDefault() { ResponseEntity response = invokeHome('curl/1.2.4', "*/*") @@ -210,13 +250,21 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra validateCurrentMetadata(json) } + private void validateMetadata(ResponseEntity response, MediaType mediaType, + String version, JSONCompareMode compareMode) { + validateContentType(response, mediaType) + def json = new JSONObject(response.body) + def expected = readJson(version) + JSONAssert.assertEquals(expected, json, compareMode) + } + private void validateCurrentMetadata(ResponseEntity response) { validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) validateCurrentMetadata(new JSONObject(response.body)) } private void validateCurrentMetadata(JSONObject json) { - def expected = readJson('2.0.0') + def expected = readJson('2.1.0') JSONAssert.assertEquals(expected, json, JSONCompareMode.STRICT) } @@ -357,7 +405,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra assertNotNull(response.body) } - private JSONObject getMetadataJson() { + private JSONObject getMetadataJson() { getMetadataJson(null, null) } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy index af0147da..5dff6716 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy @@ -101,8 +101,8 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio page.artifactId = 'foo-bar' page.name = 'My project' page.description = 'A description for my project' - page.dependency('web') - page.dependency('data-jpa') + page.dependency('web').click() + page.dependency('data-jpa').click() page.generateProject.click() at HomePage def projectAssert = zipProjectAssert(from('foo-bar.zip')) @@ -126,8 +126,8 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio page.artifactId = 'groovy-project' page.name = 'My Groovy project' page.description = 'A description for my Groovy project' - page.dependency('web') - page.dependency('data-jpa') + page.dependency('web').click() + page.dependency('data-jpa').click() page.generateProject.click() at HomePage def projectAssert = zipProjectAssert(from('groovy-project.zip')) @@ -147,7 +147,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio void createSimpleGradleProject() { toHome { page.type = 'gradle-project' - page.dependency('data-jpa') + page.dependency('data-jpa').click() page.generateProject.click() at HomePage def projectAssert = zipProjectAssert(from('demo.zip')) @@ -177,7 +177,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio void createMavenBuild() { toHome { page.type = 'maven-build' - page.dependency('data-jpa') + page.dependency('data-jpa').click() page.artifactId = 'my-maven-project' page.generateProject.click() at HomePage @@ -200,6 +200,35 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio } } + @Test + void dependencyHiddenAccordingToRange() { + toHome { // bur: [1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT) + page.dependency('org.acme:bur').displayed == true + + page.bootVersion = '1.0.2.RELEASE' + page.dependency('org.acme:bur').displayed == false + page.dependency('org.acme:biz').displayed == false + page.bootVersion = '1.1.4.RELEASE' + page.dependency('org.acme:bur').displayed == true + page.dependency('org.acme:biz').displayed == false + page.bootVersion = '1.2.0.BUILD-SNAPSHOT' + page.dependency('org.acme:bur').displayed == false + page.dependency('org.acme:biz').displayed == true + } + } + + @Test + void dependencyUncheckedWhenHidden() { + toHome { + page.dependency('org.acme:bur').value() == 'org.acme:bur' + page.bootVersion = '1.0.2.RELEASE' + page.dependency('org.acme:bur').displayed == false + page.bootVersion = '1.1.4.RELEASE' + page.dependency('org.acme:bur').displayed == true + page.dependency('org.acme:bur').value() == false + } + } + private Browser toHome(Closure script) { browser.go("http://localhost:" + port + "/") browser.at HomePage diff --git a/initializr/src/test/groovy/io/spring/initializr/web/test/HomePage.groovy b/initializr/src/test/groovy/io/spring/initializr/web/test/HomePage.groovy index 08e560f6..5668f1e2 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/test/HomePage.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/test/HomePage.groovy @@ -39,7 +39,7 @@ class HomePage extends Page { language { $('form').language() } dependency { id -> - $("form").find('input', type: "checkbox", name: "style", value: id).click() + $("form").find('input', type: "checkbox", name: "style", value: id) } generateProject { $('form').find('button', name: 'generate-project') } diff --git a/initializr/src/test/resources/application-test-default.yml b/initializr/src/test/resources/application-test-default.yml index 1adbe07f..d99df2d3 100644 --- a/initializr/src/test/resources/application-test-default.yml +++ b/initializr/src/test/resources/application-test-default.yml @@ -27,6 +27,15 @@ initializr: - name: Bar id: org.acme:bar version: 2.1.0 + - name: Biz + groupId: org.acme + artifactId: biz + version: 1.3.5 + versionRange: 1.2.0.BUILD-SNAPSHOT + - name: Bur + id: org.acme:bur + version: 2.1.0 + versionRange: "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)" types: - name: Maven POM id: maven-build diff --git a/initializr/src/test/resources/metadata/test-default-2.1.0.json b/initializr/src/test/resources/metadata/test-default-2.1.0.json new file mode 100644 index 00000000..1e73c12c --- /dev/null +++ b/initializr/src/test/resources/metadata/test-default-2.1.0.json @@ -0,0 +1,196 @@ +{ + "_links": { + "maven-build": { + "href": "http://localhost:@port@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "maven-project": { + "href": "http://localhost:@port@/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "gradle-build": { + "href": "http://localhost:@port@/build.gradle?type=gradle-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + }, + "gradle-project": { + "href": "http://localhost:@port@/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", + "templated": true + } + }, + "dependencies": { + "type": "hierarchical-multi-select", + "values": [ + { + "name": "Core", + "values": [ + { + "id": "web", + "name": "Web", + "description": "Web dependency description" + }, + { + "id": "security", + "name": "Security" + }, + { + "id": "data-jpa", + "name": "Data JPA" + } + ] + }, + { + "name": "Other", + "values": [ + { + "id": "org.acme:foo", + "name": "Foo" + }, + { + "id": "org.acme:bar", + "name": "Bar" + }, + { + "id": "org.acme:biz", + "name": "Biz", + "versionRange": "1.2.0.BUILD-SNAPSHOT" + }, + { + "id": "org.acme:bur", + "name": "Bur", + "versionRange": "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)" + } + ] + } + ] + }, + "type": { + "type": "action", + "default": "maven-project", + "values": [ + { + "id": "maven-build", + "name": "Maven POM", + "action": "/pom.xml", + "tags": { + "build": "maven", + "format": "build" + } + }, + { + "id": "maven-project", + "name": "Maven Project", + "action": "/starter.zip", + "tags": { + "build": "maven", + "format": "project" + } + }, + { + "id": "gradle-build", + "name": "Gradle Config", + "action": "/build.gradle", + "tags": { + "build": "gradle", + "format": "build" + } + }, + { + "id": "gradle-project", + "name": "Gradle Project", + "action": "/starter.zip", + "tags": { + "build": "gradle", + "format": "project" + } + } + ] + }, + "packaging": { + "type": "single-select", + "default": "jar", + "values": [ + { + "id": "jar", + "name": "Jar" + }, + { + "id": "war", + "name": "War" + } + ] + }, + "javaVersion": { + "type": "single-select", + "default": "1.7", + "values": [ + { + "id": "1.6", + "name": "1.6" + }, + { + "id": "1.7", + "name": "1.7" + }, + { + "id": "1.8", + "name": "1.8" + } + ] + }, + "language": { + "type": "single-select", + "default": "java", + "values": [ + { + "id": "groovy", + "name": "Groovy" + }, + { + "id": "java", + "name": "Java" + } + ] + }, + "bootVersion": { + "type": "single-select", + "default": "1.1.4.RELEASE", + "values": [ + { + "id": "1.2.0.BUILD-SNAPSHOT", + "name": "Latest SNAPSHOT" + }, + { + "id": "1.1.4.RELEASE", + "name": "1.1.4" + }, + { + "id": "1.0.2.RELEASE", + "name": "1.0.2" + } + ] + }, + "groupId": { + "type": "text", + "default": "org.test" + }, + "artifactId": { + "type": "text", + "default": "demo" + }, + "version": { + "type": "text", + "default": "0.0.1-SNAPSHOT" + }, + "name": { + "type": "text", + "default": "demo" + }, + "description": { + "type": "text", + "default": "Demo project for Spring Boot" + }, + "packageName": { + "type": "text", + "default": "demo" + } +} \ No newline at end of file