diff --git a/initializr/src/main/groovy/io/spring/initializr/config/InitializrAutoConfiguration.groovy b/initializr/src/main/groovy/io/spring/initializr/config/InitializrAutoConfiguration.groovy index cba940f3..8c6b6433 100644 --- a/initializr/src/main/groovy/io/spring/initializr/config/InitializrAutoConfiguration.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/config/InitializrAutoConfiguration.groovy @@ -22,9 +22,11 @@ import com.google.common.cache.CacheBuilder import io.spring.initializr.generator.ProjectGenerationMetricsListener import io.spring.initializr.generator.ProjectGenerator import io.spring.initializr.generator.ProjectResourceLocator +import io.spring.initializr.metadata.DependencyMetadataProvider import io.spring.initializr.metadata.InitializrMetadataBuilder import io.spring.initializr.metadata.InitializrMetadataProvider import io.spring.initializr.metadata.InitializrProperties +import io.spring.initializr.support.DefaultDependencyMetadataProvider import io.spring.initializr.support.DefaultInitializrMetadataProvider import io.spring.initializr.web.MainController import io.spring.initializr.web.UiController @@ -67,19 +69,19 @@ class InitializrAutoConfiguration { } @Bean - @ConditionalOnMissingBean(MainController) + @ConditionalOnMissingBean MainController initializrMainController() { new MainController() } @Bean - @ConditionalOnMissingBean(UiController) + @ConditionalOnMissingBean UiController initializrUiController() { new UiController() } @Bean - @ConditionalOnMissingBean(ProjectGenerator) + @ConditionalOnMissingBean ProjectGenerator projectGenerator() { def generator = new ProjectGenerator() generator.listeners << metricsListener() @@ -98,17 +100,24 @@ class InitializrAutoConfiguration { new DefaultInitializrMetadataProvider(metadata) } + @Bean + @ConditionalOnMissingBean + DependencyMetadataProvider dependencyMetadataProvider() { + new DefaultDependencyMetadataProvider() + } + @Bean ProjectGenerationMetricsListener metricsListener() { new ProjectGenerationMetricsListener(counterService) } @Bean - @ConditionalOnMissingBean(CacheManager) + @ConditionalOnMissingBean CacheManager cacheManager() { def cacheManager = new SimpleCacheManager() cacheManager.caches = Arrays.asList( createConcurrentMapCache(600, 'initializr'), + new ConcurrentMapCache('dependency-metadata'), new ConcurrentMapCache("project-resources")) cacheManager } diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapper.groovy new file mode 100644 index 00000000..34084a36 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapper.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.metadata.DependencyMetadata + +/** + * Generate a JSON representation of a set of dependencies. + * + * @author Stephane Nicoll + * @since 1.0 + */ +interface DependencyMetadataJsonMapper { + + /** + * Write a json representation of the specified meta-data. + */ + String write(DependencyMetadata metadata); + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataV21JsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataV21JsonMapper.groovy new file mode 100644 index 00000000..ea99869e --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/DependencyMetadataV21JsonMapper.groovy @@ -0,0 +1,85 @@ +/* + * 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 groovy.json.JsonBuilder +import io.spring.initializr.metadata.BillOfMaterials +import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.DependencyMetadata +import io.spring.initializr.metadata.Repository + +/** + * A {@link DependencyMetadataJsonMapper} handling the meta-data format for v2.1. + * + * @author Stephane Nicoll + * @since 1.0 + */ +class DependencyMetadataV21JsonMapper implements DependencyMetadataJsonMapper { + + @Override + String write(DependencyMetadata metadata) { + JsonBuilder json = new JsonBuilder() + json { + bootVersion metadata.bootVersion.toString() + dependencies metadata.dependencies.collectEntries { id, d -> + [id, mapDependency(d)] + } + repositories metadata.repositories.collectEntries { id, r -> [id, mapRepository(r)] } + boms metadata.boms.collectEntries { id, b -> [id, mapBom(b)] } + } + json.toString() + } + + private static mapDependency(Dependency dep) { + def result = [:] + result.groupId = dep.groupId + result.artifactId = dep.artifactId + if (dep.version) { + result.version = dep.version + } + result.scope = dep.scope + if (dep.bom) { + result.bom = dep.bom + } + if (dep.repository) { + result.repository = dep.repository + } + result + } + + private static mapRepository(Repository repo) { + def result = [:] + result.name = repo.name + result.url = repo.url + result.snapshotEnabled = repo.snapshotsEnabled + result + } + + private static mapBom(BillOfMaterials bom) { + def result = [:] + result.groupId = bom.groupId + result.artifactId = bom.artifactId + if (bom.version) { + result.version = bom.version + } + if (bom.repositories) { + result.repositories = bom.repositories + } + result + } + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy index e8c11587..e96a6679 100644 --- a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataV21JsonMapper.groovy @@ -16,17 +16,37 @@ package io.spring.initializr.mapper +import org.springframework.hateoas.TemplateVariable +import org.springframework.hateoas.TemplateVariables +import org.springframework.hateoas.UriTemplate + /** * 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. + * the Spring Boot versions that can be used against it. That version also adds + * an additional `dependencies` endpoint. * * @author Stephane Nicoll * @since 1.0 */ class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMapper { + private final TemplateVariables dependenciesVariables + + InitializrMetadataV21JsonMapper() { + this.dependenciesVariables = new TemplateVariables( + new TemplateVariable('bootVersion', TemplateVariable.VariableType.REQUEST_PARAM) + ) + } + + @Override + protected links(parent, types, appUrl) { + def links = super.links(parent, types, appUrl) + links['dependencies'] = dependenciesLink(appUrl) + links + } + @Override protected mapDependency(dependency) { def content = mapValue(dependency) @@ -35,4 +55,13 @@ class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMapper { } content } + + private dependenciesLink(appUrl) { + String uri = appUrl != null ? appUrl + '/dependencies' : '/dependencies' + UriTemplate uriTemplate = new UriTemplate(uri, this.dependenciesVariables) + def result = [:] + result.href = uriTemplate.toString() + result.templated = true + result + } } diff --git a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataVersion.groovy b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataVersion.groovy index dfe8bbef..c9d986d9 100644 --- a/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataVersion.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/mapper/InitializrMetadataVersion.groovy @@ -33,7 +33,8 @@ enum InitializrMetadataVersion { /** * Add 'versionRange' attribute to any dependency to specify which - * Spring Boot versions are compatible with it. + * Spring Boot versions are compatible with it. Also provide a + * separate 'dependencies' endpoint to query dependencies meta-data. */ V2_1('application/vnd.initializr.v2.1+json') diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/BillOfMaterials.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/BillOfMaterials.groovy index 38ecdfd1..a00e0162 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/BillOfMaterials.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/BillOfMaterials.groovy @@ -29,7 +29,7 @@ import io.spring.initializr.util.VersionRange * @author Stephane Nicoll * @since 1.0 */ -@ToString(ignoreNulls = true, includePackage = false) +@ToString(ignoreNulls = true, excludes = 'mappings', includePackage = false) @JsonInclude(JsonInclude.Include.NON_NULL) class BillOfMaterials { @@ -79,6 +79,7 @@ class BillOfMaterials { throw new IllegalStateException("No suitable mapping was found for $this and version $bootVersion") } + @ToString(ignoreNulls = true, includePackage = false) static class Mapping { String versionRange diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy index b3620d24..e0d2b414 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy @@ -179,6 +179,16 @@ class Dependency extends MetadataElement { return this } + /** + * Specify if this dependency is available for the specified Spring Boot version. + */ + boolean match(Version version) { + if (versionRange) { + return VersionRange.parse(versionRange).match(version) + } + true + } + /** * Generate an id using the groupId and artifactId */ diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadata.groovy new file mode 100644 index 00000000..c8f9ec01 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadata.groovy @@ -0,0 +1,45 @@ +/* + * 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.metadata + +import io.spring.initializr.util.Version + +/** + * Dependency meta-data for a given spring boot {@link Version}. + * + * @author Stephane Nicoll + * @since 1.0 + */ +class DependencyMetadata { + + final Version bootVersion + + final Map dependencies + + final Map repositories + + final Map boms + + DependencyMetadata(Version bootVersion, Map dependencies, + Map repositories, Map boms) { + this.bootVersion = bootVersion + this.dependencies = dependencies + this.repositories = repositories + this.boms = boms + } + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadataProvider.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadataProvider.groovy new file mode 100644 index 00000000..50b57ae2 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/DependencyMetadataProvider.groovy @@ -0,0 +1,33 @@ +/* + * 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.metadata + +import io.spring.initializr.util.Version + +/** + * Provide the {@link DependencyMetadata} for a given spring boot version. + * + * @author Stephane Nicoll + */ +interface DependencyMetadataProvider { + + /** + * Return the dependency metadata to use for the specified {@code bootVersion}. + */ + DependencyMetadata get(InitializrMetadata metadata, Version bootVersion) + +} \ No newline at end of file diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy index e0c55791..92e7cffb 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy @@ -15,6 +15,7 @@ */ package io.spring.initializr.metadata + /** * Meta-data used to generate a project. * @@ -44,7 +45,7 @@ class InitializrMetadata { final TextCapability name = new TextCapability('name', 'Name', 'project name (infer application name)') - final TextCapability description = new TextCapability('description', 'Description', 'project description' ) + final TextCapability description = new TextCapability('description', 'Description', 'project description') final TextCapability groupId = new TextCapability('groupId', 'Group', 'project coordinates') @@ -89,18 +90,36 @@ class InitializrMetadata { this.configuration.validate() dependencies.validate() - for (Dependency dependency : dependencies.all) { + def repositories = configuration.env.repositories + dependencies.all.forEach { dependency -> 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") } - def repositories = configuration.env.repositories + if (dependency.repository && !repositories[dependency.repository]) { throw new InvalidInitializrMetadataException("Dependency $dependency " + "defines an invalid repository id $dependency.repository, available repositores $repositories") } } + configuration.env.boms.values().forEach { bom -> + bom.repositories.forEach { r -> + if (!repositories[r]) { + throw new InvalidInitializrMetadataException("$bom " + + "defines an invalid repository id $r, available repositores $repositories") + } + } + bom.mappings.forEach{ m -> + m.repositories.forEach { r -> + if (!repositories[r]) { + throw new InvalidInitializrMetadataException("$m of $bom " + + "defines an invalid repository id $r, available repositores $repositories") + } + } + } + } + } /** diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/Repository.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/Repository.groovy index fb04122f..bb411579 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/Repository.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/Repository.groovy @@ -16,6 +16,9 @@ package io.spring.initializr.metadata +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + /** * Define a repository to be represented in the generated project * if a dependency refers to it. @@ -23,6 +26,8 @@ package io.spring.initializr.metadata * @author Stephane Nicoll * @since 1.0 */ +@EqualsAndHashCode +@ToString(includePackage = false) class Repository { String name diff --git a/initializr/src/main/groovy/io/spring/initializr/support/DefaultDependencyMetadataProvider.groovy b/initializr/src/main/groovy/io/spring/initializr/support/DefaultDependencyMetadataProvider.groovy new file mode 100644 index 00000000..2ccb0d30 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/support/DefaultDependencyMetadataProvider.groovy @@ -0,0 +1,70 @@ +/* + * 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 io.spring.initializr.metadata.BillOfMaterials +import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.DependencyMetadata +import io.spring.initializr.metadata.DependencyMetadataProvider +import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.metadata.Repository +import io.spring.initializr.util.Version + +import org.springframework.cache.annotation.Cacheable + +/** + * A default {@link DependencyMetadataProvider} implementation. + * + * @author Stephane Nicoll + * @since 1.0 + */ +class DefaultDependencyMetadataProvider implements DependencyMetadataProvider { + + @Override + @Cacheable(cacheNames = "dependency-metadata", key = "#p1") + DependencyMetadata get(InitializrMetadata metadata, Version bootVersion) { + Map dependencies = [:] + metadata.dependencies.getAll().forEach { d -> + if (d.match(bootVersion)) { + dependencies[d.id] = d.resolve(bootVersion) + } + } + + Map repositories = [:] + dependencies.values().forEach { d -> + if (d.repository) { + repositories[d.repository] = metadata.configuration.env.repositories[d.repository] + } + } + + Map boms = [:] + dependencies.values().forEach { d -> + if (d.bom) { + boms[d.bom] = metadata.configuration.env.boms.get(d.bom).resolve(bootVersion) + } + } + // Each resolved bom may require additional repositories + boms.values().forEach { b -> + b.repositories.forEach { id -> + repositories[id] = metadata.configuration.env.repositories[id] + } + } + + return new DependencyMetadata(bootVersion, dependencies, repositories, boms) + } + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/util/Version.groovy b/initializr/src/main/groovy/io/spring/initializr/util/Version.groovy index 9e017236..48333aa1 100644 --- a/initializr/src/main/groovy/io/spring/initializr/util/Version.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/util/Version.groovy @@ -37,7 +37,8 @@ import org.springframework.util.Assert * @since 1.0 */ @EqualsAndHashCode -class Version implements Comparable { +@SuppressWarnings("serial") +final class Version implements Serializable, Comparable { private static final String VERSION_REGEX = '^(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.([^0-9]+)(\\d+)?)?$' 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 ccba3016..3a0a52e9 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy @@ -21,13 +21,16 @@ import java.util.concurrent.TimeUnit import groovy.util.logging.Slf4j import io.spring.initializr.generator.CommandLineHelpGenerator +import io.spring.initializr.generator.ProjectGenerator +import io.spring.initializr.generator.ProjectRequest +import io.spring.initializr.mapper.DependencyMetadataV21JsonMapper import io.spring.initializr.mapper.InitializrMetadataJsonMapper import io.spring.initializr.mapper.InitializrMetadataV21JsonMapper import io.spring.initializr.mapper.InitializrMetadataV2JsonMapper import io.spring.initializr.mapper.InitializrMetadataVersion -import io.spring.initializr.generator.ProjectGenerator -import io.spring.initializr.generator.ProjectRequest +import io.spring.initializr.metadata.DependencyMetadataProvider import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.util.Version import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.CacheControl @@ -40,6 +43,7 @@ import org.springframework.util.DigestUtils import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseBody /** @@ -60,6 +64,9 @@ class MainController extends AbstractInitializrController { @Autowired private ProjectGenerator projectGenerator + @Autowired + private DependencyMetadataProvider dependencyMetadataProvider + private CommandLineHelpGenerator commandLineHelpGenerator = new CommandLineHelpGenerator() @@ -140,6 +147,21 @@ class MainController extends AbstractInitializrController { } } + @RequestMapping(value = "/dependencies", produces = ["application/vnd.initializr.v2.1+json", "application/json"]) + ResponseEntity dependenciesV21(@RequestParam(required = false) String bootVersion) { + dependenciesFor(InitializrMetadataVersion.V2_1, bootVersion) + } + + private ResponseEntity dependenciesFor(InitializrMetadataVersion version, String bootVersion) { + def metadata = metadataProvider.get() + Version v = bootVersion != null ? Version.parse(bootVersion) : + Version.parse(metadata.bootVersions.getDefault().id); + def dependencyMetadata = dependencyMetadataProvider.get(metadata, v) + def content = new DependencyMetadataV21JsonMapper().write(dependencyMetadata) + return ResponseEntity.ok().contentType(version.mediaType).eTag(createUniqueId(content)) + .cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)).body(content) + } + @RequestMapping(value = '/', produces = 'text/html') @ResponseBody String home() { diff --git a/initializr/src/test/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapperTests.groovy b/initializr/src/test/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapperTests.groovy new file mode 100644 index 00000000..4e441853 --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/mapper/DependencyMetadataJsonMapperTests.groovy @@ -0,0 +1,56 @@ +/* + * 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 groovy.json.JsonSlurper +import io.spring.initializr.metadata.BillOfMaterials +import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.DependencyMetadata +import io.spring.initializr.metadata.Repository +import io.spring.initializr.util.Version +import org.junit.Test + +import static org.junit.Assert.* + +/** + * @author Stephane Nicoll + */ +class DependencyMetadataJsonMapperTests { + + private final DependencyMetadataJsonMapper mapper = new DependencyMetadataV21JsonMapper() + private final JsonSlurper slurper = new JsonSlurper() + + @Test + void mapDependency() { + Dependency d = new Dependency(id: 'foo', groupId: 'org.foo', artifactId: 'foo', + repository: 'my-repo', bom: 'my-bom') + Repository repository = new Repository(name: 'foo-repo', + url: new URL('http://example.com/foo')) + BillOfMaterials bom = new BillOfMaterials('groupId': 'org.foo', + artifactId: 'foo-bom', version: '1.0.0.RELEASE') + DependencyMetadata metadata = new DependencyMetadata(Version.parse('1.2.0.RELEASE'), + [(d.id): d], ['repo-id': repository], ['bom-id': bom]) + def content = slurper.parseText(mapper.write(metadata)) + println content + assertEquals 'my-bom', content.dependencies['foo'].bom + assertEquals 'my-repo', content.dependencies['foo'].repository + assertEquals 'foo-repo', content.repositories['repo-id'].name + assertEquals 'foo-bom', content.boms['bom-id'].artifactId + assertEquals '1.0.0.RELEASE', content.boms['bom-id'].version + } + +} diff --git a/initializr/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy b/initializr/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy index daedf329..15cafed8 100644 --- a/initializr/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy @@ -68,6 +68,20 @@ class InitializrMetadataTests { builder.build() } + @Test + void invalidBomUnknownRepository() { + def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom', + version: '1.0.0.RELEASE', repositories: ['foo-repo']) + + InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder + .withDefaults().addBom('foo-bom', bom) + + thrown.expect(InvalidInitializrMetadataException) + thrown.expectMessage("invalid repository id foo-repo") + thrown.expectMessage("foo-bom") + builder.build() + } + @Test void invalidBomVersionRangeMapping() { def bom = new BillOfMaterials(groupId: 'org.acme', artifactId: 'foo-bom') @@ -83,4 +97,20 @@ class InitializrMetadataTests { builder.build() } + @Test + void invalidBomVersionRangeMappingUnknownRepo() { + 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', repositories: ['foo-repo']) + + InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder + .withDefaults().addBom('foo-bom', bom) + + thrown.expect(InvalidInitializrMetadataException) + thrown.expectMessage("invalid repository id foo-repo") + thrown.expectMessage('1.3.0.M2') + thrown.expectMessage("foo-bom") + builder.build() + } + } diff --git a/initializr/src/test/groovy/io/spring/initializr/support/DefaultDependencyMetadataProviderTests.groovy b/initializr/src/test/groovy/io/spring/initializr/support/DefaultDependencyMetadataProviderTests.groovy new file mode 100644 index 00000000..1971bcb3 --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/support/DefaultDependencyMetadataProviderTests.groovy @@ -0,0 +1,166 @@ +/* + * 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 io.spring.initializr.metadata.BillOfMaterials +import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.DependencyMetadata +import io.spring.initializr.metadata.DependencyMetadataProvider +import io.spring.initializr.test.InitializrMetadataTestBuilder +import io.spring.initializr.util.Version +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame; + +/** + * @author Stephane Nicoll + */ +class DefaultDependencyMetadataProviderTests { + + private final DependencyMetadataProvider provider = new DefaultDependencyMetadataProvider() + + @Test + void filterDependencies() { + def first = new Dependency(id: 'first', groupId: 'org.foo', artifactId: 'first', + versionRange: '1.1.4.RELEASE') + def second = new Dependency(id: 'second', groupId: 'org.foo', artifactId: 'second') + def third = new Dependency(id: 'third', groupId: 'org.foo', artifactId: 'third', + versionRange: '1.1.8.RELEASE') + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addDependencyGroup('test', first, second, third).build() + def dependencyMetadata = provider.get(metadata, Version.parse('1.1.5.RELEASE')) + assertEquals 2, dependencyMetadata.dependencies.size() + assertEquals 0, dependencyMetadata.repositories.size() + assertEquals 0, dependencyMetadata.boms.size() + assertSame first, dependencyMetadata.dependencies['first'] + assertSame second, dependencyMetadata.dependencies['second'] + } + + @Test + void resolveDependencies() { + def first = new Dependency(id: 'first', groupId: 'org.foo', artifactId: 'first') + first.versions << new Dependency.Mapping(versionRange: '[1.0.0.RELEASE, 1.1.0.RELEASE)', + version: '0.1.0.RELEASE') + first.versions << new Dependency.Mapping(versionRange: '1.1.0.RELEASE', + version: '0.2.0.RELEASE') + def second = new Dependency(id: 'second', groupId: 'org.foo', artifactId: 'second') + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addDependencyGroup('test', first, second).build() + + def dependencyMetadata = provider.get(metadata, Version.parse('1.0.5.RELEASE')) + assertEquals 2, dependencyMetadata.dependencies.size() + assertEquals('0.1.0.RELEASE', dependencyMetadata.dependencies['first'].version) + + def anotherDependencyMetadata = provider.get(metadata, Version.parse('1.1.0.RELEASE')) + assertEquals 2, anotherDependencyMetadata.dependencies.size() + assertEquals('0.2.0.RELEASE', anotherDependencyMetadata.dependencies['first'].version) + } + + @Test + void addRepoAndRemoveDuplicates() { + def first = new Dependency(id: 'first', groupId: 'org.foo', artifactId: 'first', + repository: 'repo-foo') + def second = new Dependency(id: 'second', groupId: 'org.foo', artifactId: 'second') + def third = new Dependency(id: 'third', groupId: 'org.foo', artifactId: 'third', + repository: 'repo-foo') + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addRepository('repo-foo', 'my-repo', 'http://localhost', false) + .addDependencyGroup('test', first, second, third).build() + def dependencyMetadata = provider.get(metadata, Version.parse('1.1.5.RELEASE')) + assertEquals 3, dependencyMetadata.dependencies.size() + assertEquals 1, dependencyMetadata.repositories.size() + assertEquals 0, dependencyMetadata.boms.size() + assertSame metadata.configuration.env.repositories.get('repo-foo'), + dependencyMetadata.repositories['repo-foo'] + } + + @Test + void addBomAndRemoveDuplicates() { + def first = new Dependency(id: 'first', groupId: 'org.foo', artifactId: 'first', + bom: 'bom-foo') + def second = new Dependency(id: 'second', groupId: 'org.foo', artifactId: 'second') + def third = new Dependency(id: 'third', groupId: 'org.foo', artifactId: 'third', + bom: 'bom-foo') + + def bom = new BillOfMaterials(groupId: 'org.foo', artifactId: 'bom') + bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.0.0.RELEASE, 1.1.8.RELEASE)', + version: '1.0.0.RELEASE') + bom.mappings << new BillOfMaterials.Mapping(versionRange: '1.1.8.RELEASE', + version: '2.0.0.RELEASE') + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addBom('bom-foo',bom) + .addDependencyGroup('test', first, second, third).build() + def dependencyMetadata = provider.get(metadata, Version.parse('1.1.5.RELEASE')) + assertEquals 3, dependencyMetadata.dependencies.size() + assertEquals 0, dependencyMetadata.repositories.size() + assertEquals 1, dependencyMetadata.boms.size() + assertEquals 'org.foo', dependencyMetadata.boms['bom-foo'].groupId + assertEquals 'bom', dependencyMetadata.boms['bom-foo'].artifactId + assertEquals '1.0.0.RELEASE', dependencyMetadata.boms['bom-foo'].version + } + + @Test + void repoFromBomAccordingToVersion() { + def dependencyMetadata = testRepoFromBomAccordingToVersion('1.0.9.RELEASE') + assertEquals(Version.parse('1.0.9.RELEASE'), dependencyMetadata.bootVersion) + assertEquals 3, dependencyMetadata.dependencies.size() + assertEquals 2, dependencyMetadata.repositories.size() + assertEquals 1, dependencyMetadata.boms.size() + assertEquals 'foo', dependencyMetadata.repositories['repo-foo'].name + assertEquals 'bar', dependencyMetadata.repositories['repo-bar'].name + assertEquals 'org.foo', dependencyMetadata.boms['bom-foo'].groupId + assertEquals 'bom', dependencyMetadata.boms['bom-foo'].artifactId + assertEquals '2.0.0.RELEASE', dependencyMetadata.boms['bom-foo'].version + } + + @Test + void repoFromBomAccordingToAnotherVersion() { + def dependencyMetadata = testRepoFromBomAccordingToVersion('1.1.5.RELEASE') + assertEquals(Version.parse('1.1.5.RELEASE'), dependencyMetadata.bootVersion) + assertEquals 3, dependencyMetadata.dependencies.size() + assertEquals 2, dependencyMetadata.repositories.size() + assertEquals 1, dependencyMetadata.boms.size() + assertEquals 'foo', dependencyMetadata.repositories['repo-foo'].name + assertEquals 'biz', dependencyMetadata.repositories['repo-biz'].name + assertEquals 'org.foo', dependencyMetadata.boms['bom-foo'].groupId + assertEquals 'bom', dependencyMetadata.boms['bom-foo'].artifactId + assertEquals '3.0.0.RELEASE', dependencyMetadata.boms['bom-foo'].version + } + + private DependencyMetadata testRepoFromBomAccordingToVersion(bootVersion) { + def first = new Dependency(id: 'first', groupId: 'org.foo', artifactId: 'first', + repository: 'repo-foo') + def second = new Dependency(id: 'second', groupId: 'org.foo', artifactId: 'second') + def third = new Dependency(id: 'third', groupId: 'org.foo', artifactId: 'third', + bom: 'bom-foo') + + BillOfMaterials bom = new BillOfMaterials(groupId: 'org.foo', artifactId: 'bom') + bom.mappings << new BillOfMaterials.Mapping(versionRange: '[1.0.0.RELEASE, 1.1.0.RELEASE)', + version: '2.0.0.RELEASE', repositories: ['repo-foo', 'repo-bar']) + bom.mappings << new BillOfMaterials.Mapping(versionRange: '1.1.0.RELEASE', + version: '3.0.0.RELEASE', repositories: ['repo-biz']) + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addBom('bom-foo', bom) + .addRepository('repo-foo', 'foo', 'http://localhost', false) + .addRepository('repo-bar', 'bar', 'http://localhost', false) + .addRepository('repo-biz', 'biz', 'http://localhost', false) + .addDependencyGroup('test', first, second, third).build() + provider.get(metadata, Version.parse(bootVersion)) + } + +} diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerDependenciesTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerDependenciesTests.groovy new file mode 100644 index 00000000..6308370c --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerDependenciesTests.groovy @@ -0,0 +1,60 @@ +/* + * 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.web + +import org.json.JSONObject +import org.junit.Test +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode + +import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity +import org.springframework.test.context.ActiveProfiles + +import static org.hamcrest.CoreMatchers.nullValue +import static org.hamcrest.core.IsNot.not +import static org.junit.Assert.assertThat + +/** + * @author Stephane Nicoll + */ +@ActiveProfiles('test-default') +class MainControllerDependenciesTests extends AbstractInitializrControllerIntegrationTests { + + @Test + void noBootVersion() { + ResponseEntity response = execute('/dependencies', String, null, 'application/json') + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) + validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) + validateDependenciesOutput('1.1.4', new JSONObject(response.body)) + } + + @Test + void filteredDependencies() { + ResponseEntity response = execute('/dependencies?bootVersion=1.2.1.RELEASE', + String, null, 'application/json') + assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue())) + validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) + validateDependenciesOutput('1.2.1', new JSONObject(response.body)) + } + + protected void validateDependenciesOutput(String version, JSONObject actual) { + def expected = readJsonFrom("metadata/dependencies/test-dependencies-$version" + ".json") + JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT) + } + +} diff --git a/initializr/src/test/groovy/io/spring/initializr/web/UiControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/UiControllerIntegrationTests.groovy index 312b30f5..2f078e85 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/UiControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/UiControllerIntegrationTests.groovy @@ -46,7 +46,7 @@ class UiControllerIntegrationTests extends AbstractInitializrControllerIntegrati } protected void validateDependenciesOutput(String version, JSONObject actual) { - def expected = readJsonFrom("metadata/dependencies/test-dependencies-$version" + ".json") + def expected = readJsonFrom("metadata/ui/test-dependencies-$version" + ".json") JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT) } diff --git a/initializr/src/test/resources/application-test-default.yml b/initializr/src/test/resources/application-test-default.yml index bfca8d7e..ffde9383 100644 --- a/initializr/src/test/resources/application-test-default.yml +++ b/initializr/src/test/resources/application-test-default.yml @@ -3,6 +3,25 @@ info: version: 1.1.4.RELEASE initializr: + env: + boms: + my-api-bom: + groupId: org.acme + artifactId: my-api-bom + mappings: + - versionRange: "[1.0.0.RELEASE,1.1.6.RELEASE)" + version: 1.0.0.RELEASE + repositories: my-api-repo-1 + - versionRange: "1.2.1.RELEASE" + version: 2.0.0.RELEASE + repositories: my-api-repo-2 + repositories: + my-api-repo-1: + name: repo1 + url: http://example.com/repo1 + my-api-repo-2: + name: repo2 + url: http://example.com/repo2 dependencies: - name: Core content: @@ -46,6 +65,7 @@ initializr: groupId: org.acme artifactId: my-api scope: provided + bom: my-api-bom types: - name: Maven POM id: maven-build diff --git a/initializr/src/test/resources/metadata/config/test-default.json b/initializr/src/test/resources/metadata/config/test-default.json index 2455670b..f3d694b2 100644 --- a/initializr/src/test/resources/metadata/config/test-default.json +++ b/initializr/src/test/resources/metadata/config/test-default.json @@ -31,7 +31,6 @@ }, "configuration": {"env": { "artifactRepository": "https://repo.spring.io/release/", - "boms": {}, "fallbackApplicationName": "Application", "forceSsl": true, "invalidApplicationNames": [ @@ -39,6 +38,16 @@ "SpringBootApplication" ], "repositories": { + "my-api-repo-1": { + "name": "repo1", + "url": "http://example.com/repo1", + "snapshotsEnabled": false + }, + "my-api-repo-2": { + "name": "repo2", + "url": "http://example.com/repo2", + "snapshotsEnabled": false + }, "spring-milestones": { "name": "Spring Milestones", "snapshotsEnabled": false, @@ -50,6 +59,29 @@ "url": "https://repo.spring.io/snapshot" } }, + "boms": { + "my-api-bom": { + "groupId": "org.acme", + "artifactId": "my-api-bom", + "repositories": [], + "mappings": [ + { + "versionRange": "[1.0.0.RELEASE,1.1.6.RELEASE)", + "repositories": [ + "my-api-repo-1" + ], + "version": "1.0.0.RELEASE" + }, + { + "versionRange": "1.2.1.RELEASE", + "repositories": [ + "my-api-repo-2" + ], + "version": "2.0.0.RELEASE" + } + ] + } + }, "springBootMetadataUrl": "https://spring.io/project_metadata/spring-boot" }}, "dependencies": { @@ -134,7 +166,8 @@ "groupId": "org.acme", "id": "my-api", "name": "My API", - "scope": "provided" + "scope": "provided", + "bom": "my-api-bom" } ], "name": "Other" diff --git a/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.1.4.json b/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.1.4.json new file mode 100644 index 00000000..53880feb --- /dev/null +++ b/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.1.4.json @@ -0,0 +1,61 @@ +{ + "bootVersion": "1.1.4.RELEASE", + "repositories": { + "my-api-repo-1": { + "name": "repo1", + "url": "http://example.com/repo1", + "snapshotEnabled": false + } + }, + "boms": { + "my-api-bom": { + "groupId": "org.acme", + "artifactId": "my-api-bom", + "version": "1.0.0.RELEASE", + "repositories": [ + "my-api-repo-1" + ] + } + }, + "dependencies": { + "web": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-web" + }, + "security": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-security" + }, + "data-jpa": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-data-jpa" + }, + "org.acme:foo": { + "groupId": "org.acme", + "scope": "compile", + "artifactId": "foo", + "version": "1.3.5" + }, + "org.acme:bar": { + "groupId": "org.acme", + "scope": "compile", + "artifactId": "bar", + "version": "2.1.0" + }, + "org.acme:bur": { + "groupId": "org.acme", + "scope": "test", + "artifactId": "bur", + "version": "2.1.0" + }, + "my-api": { + "groupId": "org.acme", + "scope": "provided", + "artifactId": "my-api", + "bom": "my-api-bom" + } + } +} \ No newline at end of file diff --git a/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.2.1.json b/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.2.1.json new file mode 100644 index 00000000..a32f02a0 --- /dev/null +++ b/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.2.1.json @@ -0,0 +1,61 @@ +{ + "bootVersion": "1.2.1.RELEASE", + "repositories": { + "my-api-repo-2": { + "name": "repo2", + "url": "http://example.com/repo2", + "snapshotEnabled": false + } + }, + "boms": { + "my-api-bom": { + "groupId": "org.acme", + "artifactId": "my-api-bom", + "version": "2.0.0.RELEASE", + "repositories": [ + "my-api-repo-2" + ] + } + }, + "dependencies": { + "web": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-web" + }, + "security": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-security" + }, + "data-jpa": { + "groupId": "org.springframework.boot", + "scope": "compile", + "artifactId": "spring-boot-starter-data-jpa" + }, + "org.acme:foo": { + "groupId": "org.acme", + "scope": "compile", + "artifactId": "foo", + "version": "1.3.5" + }, + "org.acme:bar": { + "groupId": "org.acme", + "scope": "compile", + "artifactId": "bar", + "version": "2.1.0" + }, + "org.acme:biz": { + "groupId": "org.acme", + "scope": "runtime", + "artifactId": "biz", + "version": "1.3.5" + }, + "my-api": { + "groupId": "org.acme", + "scope": "provided", + "artifactId": "my-api", + "bom": "my-api-bom" + } + } +} \ No newline at end of file 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 index 34f1469c..c06f4e0a 100644 --- a/initializr/src/test/resources/metadata/test-default-2.1.0.json +++ b/initializr/src/test/resources/metadata/test-default-2.1.0.json @@ -1,5 +1,9 @@ { "_links": { + "dependencies": { + "href": "https://localhost:@port@/dependencies{?bootVersion}", + "templated": true + }, "maven-build": { "href": "https://localhost:@port@/pom.xml?type=maven-build{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}", "templated": true diff --git a/initializr/src/test/resources/metadata/dependencies/test-dependencies-1.1.2.json b/initializr/src/test/resources/metadata/ui/test-dependencies-1.1.2.json similarity index 100% rename from initializr/src/test/resources/metadata/dependencies/test-dependencies-1.1.2.json rename to initializr/src/test/resources/metadata/ui/test-dependencies-1.1.2.json diff --git a/initializr/src/test/resources/metadata/dependencies/test-dependencies-all.json b/initializr/src/test/resources/metadata/ui/test-dependencies-all.json similarity index 100% rename from initializr/src/test/resources/metadata/dependencies/test-dependencies-all.json rename to initializr/src/test/resources/metadata/ui/test-dependencies-all.json