diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy index ee687591..077c253c 100644 --- a/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy +++ b/initializr-generator/src/main/groovy/io/spring/initializr/generator/ProjectGenerator.groovy @@ -18,7 +18,6 @@ package io.spring.initializr.generator import groovy.util.logging.Slf4j import io.spring.initializr.InitializrException -import io.spring.initializr.metadata.BillOfMaterials import io.spring.initializr.metadata.Dependency import io.spring.initializr.metadata.InitializrMetadataProvider import io.spring.initializr.util.Version @@ -28,6 +27,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.context.ApplicationEventPublisher import org.springframework.util.Assert +import static io.spring.initializr.metadata.InitializrConfiguration.Env.Maven.ParentPom import static io.spring.initializr.util.GroovyTemplate.template /** @@ -201,16 +201,11 @@ class ProjectGenerator { Assert.notNull request.bootVersion, 'boot version must not be null' def model = [:] def metadata = metadataProvider.get() - - def useCustomParent = metadata.configuration.env.customParentPomGAV - model['useCustomParentPom'] = useCustomParent != null - if (useCustomParent) { - def gavParts = useCustomParent.split(':') - model['customParentPomGroup'] = gavParts[0] - model['customParentPomArtifact'] = gavParts[1] - model['customParentPomVersion'] = gavParts[2] - request.boms.put("spring-boot", new BillOfMaterials(groupId: "org.springframework.boot", artifactId: "spring-boot-dependencies", version: request.bootVersion)) + ParentPom parentPom = metadata.configuration.env.maven.resolveParentPom(request.bootVersion) + if (parentPom.includeSpringBootBom && !request.boms['spring-boot']) { + request.boms['spring-boot'] = metadata.createSpringBootBom('${spring-boot.version}') } + request.resolve(metadata) // request resolved so we can log what has been requested @@ -220,6 +215,11 @@ class ProjectGenerator { request.properties.each { model[it.key] = it.value } + model['mavenParentGroupId'] = parentPom.groupId + model['mavenParentArtifactId'] = parentPom.artifactId + model['mavenParentVersion'] = parentPom.version + model['includeSpringBootBom'] = parentPom.includeSpringBootBom + model['compileDependencies'] = filterDependencies(dependencies, Dependency.SCOPE_COMPILE) model['runtimeDependencies'] = filterDependencies(dependencies, Dependency.SCOPE_RUNTIME) model['providedDependencies'] = filterDependencies(dependencies, Dependency.SCOPE_PROVIDED) diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy index 6a6eb64a..81d35e89 100644 --- a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy +++ b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy @@ -80,8 +80,7 @@ class InitializrConfiguration { String candidate = packageName.trim().split('\\W+').join('.') if (hasInvalidChar(candidate.replace('.', '')) || env.invalidPackageNames.contains(candidate)) { return defaultPackageName - } - else { + } else { candidate } } @@ -122,14 +121,6 @@ class InitializrConfiguration { */ String springBootMetadataUrl = 'https://spring.io/project_metadata/spring-boot' - /** - * The group / artifact / version of a custom parent pom to use for generated projects. - * This is only enabled if a value is expliclty provided. - * - * The value must be specified in "groupid:artifactId:versionNumber" format - */ - String customParentPomGAV - /** * Tracking code for Google Analytics. Only enabled if a value is explicitly provided. */ @@ -179,6 +170,11 @@ class InitializrConfiguration { */ final Kotlin kotlin = new Kotlin() + /** + * Maven-specific settings. + */ + final Maven maven = new Maven() + Env() { repositories['spring-snapshots'] = new Repository(name: 'Spring Snapshots', url: new URL('https://repo.spring.io/snapshot'), snapshotsEnabled: true) @@ -194,31 +190,21 @@ class InitializrConfiguration { } void validate() { - if (customParentPomGAV) { - validateGAV(customParentPomGAV); - } + maven.parent.validate() boms.each { it.value.validate() } } - /** - * validate that the GAV has 3 components in the format expected - */ - void validateGAV(String gav) { - if (gav.split(':').length != 3) - throw new InvalidInitializrMetadataException("The group:artifact:version of ${gav} is not a valid GAV (does not have exactly 3 components") - } - void merge(Env other) { artifactRepository = other.artifactRepository springBootMetadataUrl = other.springBootMetadataUrl googleAnalyticsTrackingCode = other.googleAnalyticsTrackingCode - customParentPomGAV = other.customParentPomGAV fallbackApplicationName = other.fallbackApplicationName invalidApplicationNames = other.invalidApplicationNames forceSsl = other.forceSsl kotlin.version = other.kotlin.version + maven.merge(other.maven) other.boms.each { id, bom -> if (!boms[id]) { boms[id] = bom @@ -239,6 +225,65 @@ class InitializrConfiguration { String version } + static class Maven { + + /** + * Custom parent pom to use for generated projects. + */ + final ParentPom parent = new ParentPom() + + private void merge(Maven other) { + parent.groupId = other.parent.groupId + parent.artifactId = other.parent.artifactId + parent.version = other.parent.version + parent.includeSpringBootBom = other.parent.includeSpringBootBom + } + + /** + * Resolve the parent pom to use. If no custom parent pom is set, + * the standard spring boot parent pom with the specified {@code bootVersion} + * is used. + */ + ParentPom resolveParentPom(String bootVersion) { + return parent.groupId ? parent : + new ParentPom(groupId: "org.springframework.boot", + artifactId: "spring-boot-starter-parent", version: bootVersion) + } + + static class ParentPom { + + /** + * Parent pom groupId. + */ + String groupId + + /** + * Parent pom artifactId. + */ + String artifactId + + /** + * Parent pom version. + */ + String version + + /** + * Add the "spring-boot-dependencies" BOM to the project. + */ + boolean includeSpringBootBom + + void validate() { + if (!((!groupId && !artifactId && !version) || + (groupId && artifactId && version))) { + throw new InvalidInitializrMetadataException("Custom maven pom " + + "requires groupId, artifactId and version") + } + } + + } + + } + } } diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy index f34c34d2..afb4accf 100644 --- a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy +++ b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/InitializrMetadata.groovy @@ -144,6 +144,14 @@ class InitializrMetadata { "$bootVersion/spring-boot-cli-$bootVersion-bin.$extension" } + /** + * Create a {@link BillOfMaterials} for the spring boot BOM. + */ + BillOfMaterials createSpringBootBom(String bootVersion) { + new BillOfMaterials(groupId: 'org.springframework.boot', artifactId: 'spring-boot-dependencies', + version: bootVersion) + } + /** * Return the defaults for the capabilities defined on this instance. */ diff --git a/initializr-generator/src/main/resources/META-INF/spring-configuration-metadata.json b/initializr-generator/src/main/resources/META-INF/spring-configuration-metadata.json index 34480dd4..9672d877 100644 --- a/initializr-generator/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/initializr-generator/src/main/resources/META-INF/spring-configuration-metadata.json @@ -29,6 +29,18 @@ "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env", "sourceMethod": "getKotlin()" }, + { + "name": "initializr.env.maven", + "type": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env", + "sourceMethod": "getMaven()" + }, + { + "name": "initializr.env.maven.parent", + "type": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven$ParentPom", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven", + "sourceMethod": "getParent()" + }, { "name": "initializr.group-id", "type": "io.spring.initializr.metadata.InitializrProperties$SimpleElement", @@ -169,6 +181,31 @@ "description": "Kotlin version to use.", "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Kotlin" }, + { + "name": "initializr.env.maven.parent.artifact-id", + "type": "java.lang.String", + "description": "Parent pom artifactId.", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven$ParentPom" + }, + { + "name": "initializr.env.maven.parent.include-spring-boot-bom", + "type": "java.lang.Boolean", + "description": "Add the \"spring-boot-dependencies\" BOM to the project.", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven$ParentPom", + "defaultValue": false + }, + { + "name": "initializr.env.maven.parent.group-id", + "type": "java.lang.String", + "description": "Parent pom groupId.", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven$ParentPom" + }, + { + "name": "initializr.env.maven.parent.version", + "type": "java.lang.String", + "description": "Parent pom version.", + "sourceType": "io.spring.initializr.metadata.InitializrConfiguration$Env$Maven$ParentPom" + }, { "name": "initializr.env.repositories", "type": "java.util.Map", diff --git a/initializr-generator/src/main/resources/templates/starter-pom.xml b/initializr-generator/src/main/resources/templates/starter-pom.xml index baa767ef..29829c71 100644 --- a/initializr-generator/src/main/resources/templates/starter-pom.xml +++ b/initializr-generator/src/main/resources/templates/starter-pom.xml @@ -12,22 +12,16 @@ ${description} - <% if (useCustomParentPom) { %> - ${customParentPomGroup} - ${customParentPomArtifact} - ${customParentPomVersion} - <% } else { %> - org.springframework.boot - spring-boot-starter-parent - ${bootVersion} - <% } %> + ${mavenParentGroupId} + ${mavenParentArtifactId} + ${mavenParentVersion} UTF-8 ${javaVersion}<% if (language=='kotlin') { %> - ${kotlinVersion}<% } %><% if (useCustomParentPom) { %> + ${kotlinVersion}<% } %><% if (includeSpringBootBom) { %> ${bootVersion}<%}%> diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy index a41d1eee..eaa2d90f 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/generator/ProjectGeneratorTests.groovy @@ -72,6 +72,7 @@ class ProjectGeneratorTests extends AbstractProjectGeneratorTests { def request = createProjectRequest('web') request.bootVersion = '1.0.1.BUILD-SNAPSHOT' generateMavenPom(request).hasSnapshotRepository() + .hasSpringBootParent('1.0.1.BUILD-SNAPSHOT') .hasSpringBootStarterDependency('web') } @@ -311,20 +312,37 @@ class ProjectGeneratorTests extends AbstractProjectGeneratorTests { @Test void defaultMavenPomHasSpringBootParent() { - def metadata = InitializrMetadataTestBuilder.withDefaults().build() - applyMetadata(metadata) - def request = createProjectRequest('whatever', 'web') - generateMavenPom(request).hasParent("org.springframework.boot", "spring-boot-starter-parent", request.bootVersion) + def request = createProjectRequest('web') + generateMavenPom(request).hasSpringBootParent(request.bootVersion) } @Test void mavenPomWithCustomParentPom() { - def customGAV = "com.foo:foo-parent:1.0.0-SNAPSHOT" def metadata = InitializrMetadataTestBuilder.withDefaults() - .addCustomParentPomGAV(customGAV).build() + .addDependencyGroup('core', 'web', 'security', 'data-jpa') + .setMavenParent('com.foo', 'foo-parent', '1.0.0-SNAPSHOT', false) + .build() applyMetadata(metadata) - def request = createProjectRequest('whatever', 'web') - generateMavenPom(request).hasParent("com.foo", "foo-parent", "1.0.0-SNAPSHOT") + def request = createProjectRequest('web') + generateMavenPom(request) + .hasParent('com.foo', 'foo-parent', '1.0.0-SNAPSHOT') + .hasBomsCount(0) + } + + @Test + void mavenPomWithCustomParentPomAndSpringBootBom() { + def metadata = InitializrMetadataTestBuilder.withDefaults() + .addDependencyGroup('core', 'web', 'security', 'data-jpa') + .setMavenParent('com.foo', 'foo-parent', '1.0.0-SNAPSHOT', true) + .build() + applyMetadata(metadata) + def request = createProjectRequest('web') + request.bootVersion = '1.0.2.RELEASE' + generateMavenPom(request) + .hasParent('com.foo', 'foo-parent', '1.0.0-SNAPSHOT') + .hasProperty('spring-boot.version', '1.0.2.RELEASE') + .hasBom('org.springframework.boot', 'spring-boot-dependencies', '${spring-boot.version}') + .hasBomsCount(1) } @Test @@ -454,12 +472,14 @@ class ProjectGeneratorTests extends AbstractProjectGeneratorTests { def request = createProjectRequest('foo') request.bootVersion = '1.2.5.RELEASE' generateMavenPom(request).hasDependency(foo) + .hasSpringBootParent('1.2.5.RELEASE') .hasBom('org.acme', 'foo-bom', '1.0.0') // Second version def request2 = createProjectRequest('foo') request2.bootVersion = '1.3.0.M1' generateMavenPom(request2).hasDependency(foo) + .hasSpringBootParent('1.3.0.M1') .hasBom('org.acme', 'foo-bom', '1.2.0') } @@ -480,6 +500,7 @@ class ProjectGeneratorTests extends AbstractProjectGeneratorTests { def request = createProjectRequest('foo') request.bootVersion = '1.3.0.RELEASE' generateMavenPom(request).hasDependency(foo) + .hasSpringBootParent('1.3.0.RELEASE') .hasBom('org.acme', 'foo-bom', '1.2.0') .hasRepository('foo-repo', 'repo', 'http://example.com/foo', true) .hasRepository('bar-repo', 'repo', 'http://example.com/bar', false) diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy index f09e8715..7ad6e10c 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/InitializrMetadataTests.groovy @@ -146,4 +146,15 @@ class InitializrMetadataTests { builder.build() } + @Test + void invalidParentMissingVersion() { + InitializrMetadataTestBuilder builder = InitializrMetadataTestBuilder + .withDefaults() + .setMavenParent('org.foo', 'foo-parent', null, false) + + thrown.expect(InvalidInitializrMetadataException) + thrown.expectMessage("Custom maven pom requires groupId, artifactId and version") + builder.build() + } + } diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/test/generator/PomAssert.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/test/generator/PomAssert.groovy index 8c3f9d3d..5978c6b1 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/test/generator/PomAssert.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/test/generator/PomAssert.groovy @@ -19,6 +19,7 @@ package io.spring.initializr.test.generator import io.spring.initializr.generator.ProjectRequest import io.spring.initializr.metadata.BillOfMaterials import io.spring.initializr.metadata.Dependency +import io.spring.initializr.metadata.InitializrConfiguration.Env.Maven.ParentPom import io.spring.initializr.metadata.Repository import org.custommonkey.xmlunit.SimpleNamespaceContext import org.custommonkey.xmlunit.XMLUnit @@ -30,6 +31,7 @@ import org.w3c.dom.Element import static org.junit.Assert.assertEquals import static org.junit.Assert.assertFalse import static org.junit.Assert.assertNotNull +import static org.junit.Assert.assertTrue /** * XPath assertions that are specific to a standard Maven POM. @@ -41,6 +43,8 @@ class PomAssert { final XpathEngine eng final Document doc + final ParentPom parentPom + final Map properties = [:] final Map dependencies = [:] final Map boms = [:] final Map repositories = [:] @@ -52,6 +56,8 @@ class PomAssert { def namespaceContext = new SimpleNamespaceContext(context) eng.namespaceContext = namespaceContext doc = XMLUnit.buildControlDocument(content) + this.parentPom = parseParent() + parseProperties() parseDependencies() parseBoms() parseRepositories() @@ -63,7 +69,7 @@ class PomAssert { PomAssert validateProjectRequest(ProjectRequest request) { hasGroupId(request.groupId).hasArtifactId(request.artifactId).hasVersion(request.version). hasPackaging(request.packaging).hasName(request.name).hasDescription(request.description). - hasBootVersion(request.bootVersion).hasJavaVersion(request.javaVersion) + hasJavaVersion(request.javaVersion) } PomAssert hasGroupId(String groupId) { @@ -96,18 +102,14 @@ class PomAssert { this } - PomAssert hasBootVersion(String bootVersion) { - // when using a custom parent, the bootVersion comes from a bom entry and not the parent pom version - if (!eng.evaluate(createRootNodeXPath('parent/pom:artifactId'), doc).equals("spring-boot-starter-parent")) { - hasBom("org.springframework.boot", "spring-boot-dependencies", bootVersion) - } else { - assertEquals bootVersion, eng.evaluate(createRootNodeXPath('parent/pom:version'), doc) - } + PomAssert hasJavaVersion(String javaVersion) { + assertEquals javaVersion, eng.evaluate(createPropertyNodeXpath('java.version'), doc) this } - PomAssert hasJavaVersion(String javaVersion) { - assertEquals javaVersion, eng.evaluate(createPropertyNodeXpath('java.version'), doc) + PomAssert hasProperty(String name, String value) { + assertTrue "No property $name found", properties.containsKey(name) + assertEquals "Wrong value for property $name", value, properties[name] this } @@ -142,12 +144,16 @@ class PomAssert { } PomAssert hasParent(String groupId, String artifactId, String version) { - assertEquals version, eng.evaluate(createRootNodeXPath('parent/pom:version'), doc) - assertEquals groupId, eng.evaluate(createRootNodeXPath('parent/pom:groupId'), doc) - assertEquals artifactId, eng.evaluate(createRootNodeXPath('parent/pom:artifactId'), doc) + assertEquals groupId, this.parentPom.groupId + assertEquals artifactId, this.parentPom.artifactId + assertEquals version, this.parentPom.version this } + PomAssert hasSpringBootParent(String version) { + hasParent('org.springframework.boot', 'spring-boot-starter-parent', version) + } + PomAssert hasDependency(Dependency expected) { def id = generateDependencyId(expected.groupId, expected.artifactId) def dependency = dependencies[id] @@ -230,6 +236,24 @@ class PomAssert { "/pom:project/pom:$node" } + private ParentPom parseParent() { + new ParentPom( + groupId: eng.evaluate(createRootNodeXPath('parent/pom:groupId'), doc), + artifactId: eng.evaluate(createRootNodeXPath('parent/pom:artifactId'), doc), + version: eng.evaluate(createRootNodeXPath('parent/pom:version'), doc)) + } + + private def parseProperties() { + def nodes = eng.getMatchingNodes(createRootNodeXPath('properties/*'), doc) + for (int i = 0; i < nodes.length; i++) { + def item = nodes.item(i) + if (item instanceof Element) { + def element = (Element) item + properties[element.tagName] = element.textContent + } + } + } + private def parseDependencies() { def nodes = eng.getMatchingNodes(createRootNodeXPath('dependencies/pom:dependency'), doc) for (int i = 0; i < nodes.length; i++) { diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/test/metadata/InitializrMetadataTestBuilder.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/test/metadata/InitializrMetadataTestBuilder.groovy index 8d86edca..38ca45c5 100644 --- a/initializr-generator/src/test/groovy/io/spring/initializr/test/metadata/InitializrMetadataTestBuilder.groovy +++ b/initializr-generator/src/test/groovy/io/spring/initializr/test/metadata/InitializrMetadataTestBuilder.groovy @@ -152,9 +152,14 @@ class InitializrMetadataTestBuilder { this } - InitializrMetadataTestBuilder addCustomParentPomGAV(String customGAV) { + InitializrMetadataTestBuilder setMavenParent(String groupId, String artifactId, + String version, boolean includeSpringBootBom) { builder.withCustomizer { - it.configuration.env.customParentPomGAV = customGAV + def parent = it.configuration.env.maven.parent + parent.groupId = groupId + parent.artifactId = artifactId + parent.version = version + parent.includeSpringBootBom = includeSpringBootBom } this } diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy index 2bf9eefe..bcbba438 100644 --- a/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy +++ b/initializr-web/src/test/groovy/io/spring/initializr/web/project/ProjectGenerationSmokeTests.groovy @@ -111,7 +111,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio assertSimpleProject() .isMavenProject() .pomAssert() - .hasBootVersion('1.0.2.RELEASE') + .hasSpringBootParent('1.0.2.RELEASE') .hasDependenciesCount(2) .hasSpringBootStarterRootDependency() .hasSpringBootStarterTest() diff --git a/initializr-web/src/test/resources/metadata/config/test-default.json b/initializr-web/src/test/resources/metadata/config/test-default.json index 3c737f0b..a7f7d936 100644 --- a/initializr-web/src/test/resources/metadata/config/test-default.json +++ b/initializr-web/src/test/resources/metadata/config/test-default.json @@ -36,7 +36,14 @@ "kotlin": { "version": null }, - "customParentPomGAV": null, + "maven": { + "parent": { + "groupId": null, + "artifactId": null, + "version": null, + "includeSpringBootBom": false + } + }, "googleAnalyticsTrackingCode": null, "invalidApplicationNames": [ "SpringApplication",