Add support for custom dependency

Prior to this commit, only spring boot starters can be added as project
dependency using a simple String denoting the suffix of the artifactId.
The standard 'org.springframework.boot' and 'spring-boot-starter-'
artifactId prefix were assumed.

This commit allows to define arbitrary dependencies with arbitrary
identifiers; the groupId, artifactId and version of the dependency can
be specified. Internally, all dependencies are converted to that format
even the ones defined as standard spring boot starters.

To allow that, a ProjectRequest is now resolved against the initializr
metadata. If a request defines an unknown dependency, a simple String
will be still considered a spring-boot-starter but a more complex
unknown id will lead to an exception (e.g. 'org.foo:bar').

Fixes gh-17
This commit is contained in:
Stephane Nicoll
2014-08-19 04:07:13 +02:00
parent 964aef8bdb
commit 3849a7b5b9
15 changed files with 527 additions and 114 deletions

View File

@@ -9,67 +9,67 @@ info:
initializr:
dependencies:
- name: Core
starters:
content:
- name: Security
value: security
id: security
- name: AOP
value: aop
id: aop
- name: Data
starters:
content:
- name: JDBC
value: jdbc
id: jdbc
- name: JPA
value: data-jpa
id: data-jpa
- name: MongoDB
value: data-mongodb
id: data-mongodb
- name: Redis
value: redis
id: redis
- name: Gemfire
value: data-gemfire
id: data-gemfire
- name: Solr
value: data-solr
id: data-solr
- name: I/O
starters:
content:
- name: Batch
value: batch
id: batch
- name: Integration
value: integration
id: integration
- name: AMQP
value: amqp
id: amqp
- name: Web
starters:
content:
- name: Web
value: web
id: web
- name: Websocket
value: websocket
id: websocket
- name: Rest Repositories
value: data-rest
id: data-rest
- name: Mobile
value: mobile
id: mobile
- name: Template Engines
starters:
content:
- name: Freemarker
value: freemarker
id: freemarker
- name: Velocity
value: velocity
id: velocity
- name: Groovy Templates
value: groovy-templates
id: groovy-templates
- name: Thymeleaf
value: thymeleaf
id: thymeleaf
- name: Social
starters:
content:
- name: Facebook
value: social-facebook
id: social-facebook
- name: LinkedIn
value: social-linkedin
id: social-linkedin
- name: Twitter
value: social-twitter
id: social-twitter
- name: Ops
starters:
content:
- name: Actuator
value: actuator
id: actuator
- name: Remote Shell
value: remote-shell
id: remote-shell
types:
- name: Maven POM
id: pom.xml

View File

@@ -1,7 +1,26 @@
/*
* Copyright 2012-2014 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 javax.annotation.PostConstruct
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import groovy.transform.ToString
import org.springframework.boot.context.properties.ConfigurationProperties
@@ -36,6 +55,17 @@ class InitializrMetadata {
final Defaults defaults = new Defaults()
@JsonIgnore
final Map<String, Dependency> indexedDependencies = new HashMap<String, Dependency>()
/**
* Return the {@link Dependency} with the specified id or {@code null} if
* no such dependency exists.
*/
Dependency getDependency(String id) {
return indexedDependencies.get(id)
}
/**
* Initializes a {@link ProjectRequest} instance with the defaults
* defined in this instance.
@@ -54,6 +84,54 @@ class InitializrMetadata {
request
}
/**
* Initialize and validate the configuration.
*/
@PostConstruct
void validate() {
for (DependencyGroup group : dependencies) {
for (Dependency dependency : group.getContent()) {
validateDependency(dependency)
indexDependency(dependency.id, dependency)
}
}
}
private void indexDependency(String id, Dependency dependency) {
Dependency existing = indexedDependencies.get(id)
if (existing != null) {
throw new IllegalArgumentException('Could not register ' + dependency +
': another dependency has also the "' + id + '" id ' + existing)
}
indexedDependencies.put(id, dependency)
}
static void validateDependency(Dependency dependency) {
String id = dependency.getId()
if (id == null) {
if (!dependency.hasCoordinates()) {
throw new InvalidInitializrMetadataException('Invalid dependency, ' +
'should have at least an id or a groupId/artifactId pair.')
}
dependency.generateId()
} else if (!dependency.hasCoordinates()) {
// Let's build the coordinates from the id
StringTokenizer st = new StringTokenizer(id, ':')
if (st.countTokens() == 1) { // assume spring-boot-starter
dependency.asSpringBootStarter(id)
} else if (st.countTokens() == 2 || st.countTokens() == 3) {
dependency.groupId = st.nextToken()
dependency.artifactId = st.nextToken()
if (st.hasMoreTokens()) {
dependency.version = st.nextToken()
}
} else {
throw new InvalidInitializrMetadataException('Invalid dependency, id should ' +
'have the form groupId:artifactId[:version] but got ' + id)
}
}
}
static def getDefault(List elements, String defaultValue) {
for (DefaultIdentifiableElement element : elements) {
if (element.default) {
@@ -68,10 +146,52 @@ class InitializrMetadata {
String name
final List<Map<String,Object>> starters= new ArrayList<>()
final List<Dependency> content = new ArrayList<Dependency>()
}
@ToString(ignoreNulls = true, includePackage = false)
static class Dependency extends IdentifiableElement {
@JsonIgnore
String groupId
@JsonIgnore
String artifactId
@JsonIgnore
String version
/**
* Specify if the dependency has its coordinates set, i.e. {@code groupId}
* and {@code artifactId}.
*/
boolean hasCoordinates() {
return groupId != null && artifactId != null
}
/**
* Define this dependency as a standard spring boot starter with the specified name
*/
def asSpringBootStarter(String name) {
groupId = 'org.springframework.boot'
artifactId = 'spring-boot-starter-' + name
}
/**
* Generate an id using the groupId and artifactId
*/
def generateId() {
if (groupId == null || artifactId == null) {
throw new IllegalArgumentException('Could not generate id for ' + this
+ ': at least groupId and artifactId must be set.')
}
StringBuilder sb = new StringBuilder()
sb.append(groupId).append(':').append(artifactId)
id = sb.toString()
}
}
static class Type extends DefaultIdentifiableElement {
String action

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2014 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
/**
* Thrown when the configuration defines invalid metadata.
*
* @author Stephane Nicoll
* @since 1.0
*/
class InvalidInitializrMetadataException extends RuntimeException {
InvalidInitializrMetadataException(String s) {
super(s)
}
}

View File

@@ -123,27 +123,12 @@ class ProjectGenerator {
private Map initializeModel(ProjectRequest request) {
Assert.notNull request.bootVersion, 'boot version must not be null'
if (request.packaging == 'war' && !request.isWebStyle()) {
request.style << 'web'
}
def model = [:]
request.resolve(metadata)
request.properties.each { model[it.key] = it.value }
model.styles = fixStyles(request.style)
model
}
private def fixStyles(def style) {
if (style == null || style.size() == 0) {
style = ['']
}
if (!style.class.isArray() && !(style instanceof Collection)) {
style = [style]
}
style = style.collect { it == 'jpa' ? 'data-jpa' : it }
style.collect { it == '' ? '' : '-' + it }
}
private byte[] doGenerateMavenPom(Map model) {
template 'starter-pom.xml', model
}

View File

@@ -16,6 +16,9 @@
package io.spring.initializr
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* A request to generate a project.
*
@@ -25,6 +28,8 @@ package io.spring.initializr
*/
class ProjectRequest {
private static final Logger logger = LoggerFactory.getLogger(ProjectRequest.class)
def style = []
String name
@@ -39,6 +44,37 @@ class ProjectRequest {
String packageName
String javaVersion
def dependencies = []
/**
* Resolve this instance against the specified {@link InitializrMetadata}
*/
void resolve(InitializrMetadata metadata) {
if (packaging == 'war' && !isWebStyle()) {
style << 'web'
}
if (style == null || style.size() == 0) {
style = []
}
if (!style.class.isArray() && !(style instanceof Collection)) {
style = [style]
}
style = style.collect { it == 'jpa' ? 'data-jpa' : it }
style.collect { it == '' ? '' : '-' + it }
dependencies = style.collect {
InitializrMetadata.Dependency dependency = metadata.getDependency(it)
if (dependency == null) {
if (it.contains(':')) {
throw new IllegalArgumentException('Unknown dependency ' + it + ' check project metadata')
}
logger.warn('No known dependency for style ' + it + ' assuming spring-boot-starter')
dependency = new InitializrMetadata.Dependency()
dependency.asSpringBootStarter(it)
}
dependency
}
}
boolean isWebStyle() {
style.any { webStyle(it) }
}

View File

@@ -110,10 +110,10 @@
<% dependencies.each { %>
<div class="form-group col-sm-6">
<h4>${it.name}</h4>
<% it.starters.each { %>
<% it.content.each { %>
<div class="checkbox">
<label>
<input type="checkbox" name="style" value="${it.value}">
<input type="checkbox" name="style" value="${it.id}">
${it.name}
</label>
</div><% } %>

View File

@@ -39,8 +39,8 @@ repositories {
providedRuntime
}
<% } %>
dependencies {<% styles.each { %>
compile("org.springframework.boot:spring-boot-starter${it}")<% } %><% if (language=='groovy') { %>
dependencies {<% dependencies.each { %>
compile("${it.groupId}:${it.artifactId}")<% } %><% if (language=='groovy') { %>
compile("org.codehaus.groovy:groovy")<% } %><% if (packaging=='war') { %>
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")<% } %>
testCompile("org.springframework.boot:spring-boot-starter-test")

View File

@@ -18,10 +18,11 @@
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies><% styles.each { %>
<dependencies><% dependencies.each { %>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter${it}</artifactId>
<groupId>${it.groupId}</groupId>
<artifactId>${it.artifactId}</artifactId><% if (it.version != null) { %>
<version>${it.version}</version><% } %>
</dependency><% } %><% if (language=='groovy') { %>
<dependency>
<groupId>org.codehaus.groovy</groupId>

View File

@@ -16,15 +16,146 @@
package io.spring.initializr
import io.spring.initializr.support.InitializrMetadataBuilder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import static org.junit.Assert.assertEquals
import static org.junit.Assert.*
/**
* @author Stephane Nicoll
*/
class InitializrMetadataTests {
@Rule
public final ExpectedException thrown = ExpectedException.none()
private final InitializrMetadata metadata = new InitializrMetadata()
@Test
void setCoordinatesFromId() {
InitializrMetadata.Dependency dependency = createDependency('org.foo:bar:1.2.3')
metadata.validateDependency(dependency)
assertEquals 'org.foo', dependency.groupId
assertEquals 'bar', dependency.artifactId
assertEquals '1.2.3', dependency.version
assertEquals 'org.foo:bar:1.2.3', dependency.id
}
@Test
void setCoordinatesFromIdNoVersion() {
InitializrMetadata.Dependency dependency = createDependency('org.foo:bar')
metadata.validateDependency(dependency)
assertEquals 'org.foo', dependency.groupId
assertEquals 'bar', dependency.artifactId
assertNull dependency.version
assertEquals 'org.foo:bar', dependency.id
}
@Test
void setIdFromCoordinates() {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.groupId = 'org.foo'
dependency.artifactId = 'bar'
dependency.version = '1.0'
metadata.validateDependency(dependency)
assertEquals 'org.foo:bar', dependency.id
}
@Test
void setIdFromCoordinatesNoVersion() {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.groupId = 'org.foo'
dependency.artifactId = 'bar'
metadata.validateDependency(dependency)
assertEquals 'org.foo:bar', dependency.id
}
@Test
void setIdFromSimpleName() {
InitializrMetadata.Dependency dependency = createDependency('web')
metadata.validateDependency(dependency)
assertEquals 'org.springframework.boot', dependency.groupId
assertEquals 'spring-boot-starter-web', dependency.artifactId
assertNull dependency.version
assertEquals 'web', dependency.id
}
@Test
void invalidDependency() {
thrown.expect(InvalidInitializrMetadataException)
metadata.validateDependency(new InitializrMetadata.Dependency())
}
@Test
void invalidIdFormatTooManyColons() {
InitializrMetadata.Dependency dependency = createDependency('org.foo:bar:1.0:test:external')
thrown.expect(InvalidInitializrMetadataException)
metadata.validateDependency(dependency)
}
@Test
void generateIdWithNoGroupId() {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.artifactId = 'bar'
thrown.expect(IllegalArgumentException)
dependency.generateId()
}
@Test
void generateIdWithNoArtifactId() {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.groupId = 'foo'
thrown.expect(IllegalArgumentException)
dependency.generateId()
}
@Test
void indexedDependencies() {
InitializrMetadata metadata = new InitializrMetadata()
InitializrMetadata.DependencyGroup group = new InitializrMetadata.DependencyGroup()
InitializrMetadata.Dependency dependency = createDependency('first')
group.content.add(dependency)
InitializrMetadata.Dependency dependency2 = createDependency('second')
group.content.add(dependency2)
metadata.dependencies.add(group)
metadata.validate()
assertSame dependency, metadata.getDependency('first')
assertSame dependency2, metadata.getDependency('second')
assertNull metadata.getDependency('anotherId')
}
@Test
void addTwoDependenciesWithSameId() {
InitializrMetadata metadata = new InitializrMetadata()
InitializrMetadata.DependencyGroup group = new InitializrMetadata.DependencyGroup()
InitializrMetadata.Dependency dependency = createDependency('conflict')
group.content.add(dependency)
InitializrMetadata.Dependency dependency2 = createDependency('conflict')
group.content.add(dependency2)
metadata.dependencies.add(group)
thrown.expect(IllegalArgumentException)
thrown.expectMessage('conflict')
metadata.validate()
}
@Test
void createProjectRequest() {
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults().get()
ProjectRequest request = doCreateProjectRequest(metadata)
assertEquals metadata.defaults.groupId, request.groupId
}
@Test
void getDefaultNoDefault() {
List elements = []
@@ -39,10 +170,23 @@ class InitializrMetadataTests {
assertEquals 'two', InitializrMetadata.getDefault(elements, 'three')
}
private static ProjectRequest doCreateProjectRequest(InitializrMetadata metadata) {
ProjectRequest request = new ProjectRequest()
metadata.initializeProjectRequest(request)
request
}
private static InitializrMetadata.Dependency createDependency(String id) {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.id = id
dependency
}
private static InitializrMetadata.JavaVersion createJavaVersion(String version, boolean selected) {
InitializrMetadata.JavaVersion javaVersion = new InitializrMetadata.JavaVersion()
javaVersion.id = version
javaVersion.default = selected
javaVersion
}
}

View File

@@ -50,19 +50,6 @@ class ProjectGeneratorTests {
.hasSnapshotRepository().hasSpringBootStarterDependency('web')
}
@Test
void mavenWarPomWithoutWebFacet() {
ProjectRequest request = createProjectRequest('data-jpa')
request.packaging = 'war'
generateMavenPom(request).hasStartClass('demo.Application')
.hasSpringBootStarterDependency('tomcat')
.hasSpringBootStarterDependency('data-jpa')
.hasSpringBootStarterDependency('web') // Added by web facet
.hasSpringBootStarterDependency('test')
.hasDependenciesCount(4)
}
PomAssert generateMavenPom(ProjectRequest request) {
String content = new String(projectGenerator.generateMavenPom(request))
return new PomAssert(content).validateProjectRequest(request)

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2014 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 io.spring.initializr.support.InitializrMetadataBuilder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import static org.junit.Assert.assertEquals
/**
* @author Stephane Nicoll
*/
class ProjectRequestTests {
@Rule
public final ExpectedException thrown = ExpectedException.none()
@Test
void resolve() {
ProjectRequest request = new ProjectRequest()
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('code', 'web', 'security', 'spring-data').get()
request.style << 'web' << 'spring-data'
request.resolve(metadata)
assertBootStarter(request.dependencies.get(0), 'web')
assertBootStarter(request.dependencies.get(1), 'spring-data')
}
@Test
void resolveFullMetadata() {
ProjectRequest request = new ProjectRequest()
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('code', createDependency('org.foo', 'acme', '1.2.0')).get()
request.style << 'org.foo:acme'
request.resolve(metadata)
assertDependency(request.dependencies.get(0), 'org.foo', 'acme', '1.2.0')
}
@Test
void resolveUnknownSimpleIdAsSpringBootStarter() {
ProjectRequest request = new ProjectRequest()
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('code', 'org.foo:bar').get()
request.style << 'org.foo:bar' << 'foo-bar'
request.resolve(metadata)
assertDependency(request.dependencies.get(0), 'org.foo', 'bar', null)
assertBootStarter(request.dependencies.get(1), 'foo-bar')
}
@Test
void resolveUnknownDependency() {
ProjectRequest request = new ProjectRequest()
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('code', 'org.foo:bar').get()
request.style << 'org.foo:acme' // does not exist and
thrown.expect(IllegalArgumentException)
thrown.expectMessage('org.foo:acme')
request.resolve(metadata)
}
private static void assertBootStarter(InitializrMetadata.Dependency actual, String name) {
InitializrMetadata.Dependency expected = new InitializrMetadata.Dependency()
expected.asSpringBootStarter(name)
assertDependency(actual, expected.groupId, expected.artifactId, expected.version)
}
private static InitializrMetadata.Dependency createDependency(String groupId, String artifactId, String version) {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.groupId = groupId
dependency.artifactId = artifactId
dependency.version = version
dependency
}
private static void assertDependency(InitializrMetadata.Dependency actual, String groupId,
String artifactId, String version) {
assertEquals groupId, actual.groupId
assertEquals artifactId, actual.artifactId
assertEquals version, actual.version
}
}

View File

@@ -34,6 +34,7 @@ class InitializrMetadataBuilder {
}
InitializrMetadata get() {
metadata.validate()
metadata
}
@@ -41,15 +42,22 @@ class InitializrMetadataBuilder {
InitializrMetadata.DependencyGroup group = new InitializrMetadata.DependencyGroup()
group.name = name
for (String id : ids) {
Map<String, Object> starter = new HashMap<>()
starter.put('name', id)
starter.put('value', id)
group.starters.add(starter)
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.id = id
group.content.add(dependency)
}
metadata.dependencies.add(group)
this
}
InitializrMetadataBuilder addDependencyGroup(String name, InitializrMetadata.Dependency... dependencies) {
InitializrMetadata.DependencyGroup group = new InitializrMetadata.DependencyGroup()
group.name = name
group.content.addAll(dependencies)
metadata.dependencies.add(group)
this
}
InitializrMetadataBuilder addDefaults() {
addDefaultTypes().addDefaultPackagings().addDefaultJavaVersions()
.addDefaultLanguages().addDefaultBootVersions()

View File

@@ -16,6 +16,7 @@
package io.spring.initializr.support
import io.spring.initializr.InitializrMetadata
import io.spring.initializr.ProjectRequest
import org.custommonkey.xmlunit.SimpleNamespaceContext
import org.custommonkey.xmlunit.XMLUnit
@@ -36,7 +37,7 @@ class PomAssert {
final XpathEngine eng
final Document doc
final Map<String, Dependency> dependencies = new HashMap<String, Dependency>()
final Map<String, InitializrMetadata.Dependency> dependencies = new HashMap<String, InitializrMetadata.Dependency>()
PomAssert(String content) {
eng = XMLUnit.newXpathEngine()
@@ -170,7 +171,7 @@ class PomAssert {
for (int i = 0; i < nodes.length; i++) {
def item = nodes.item(i)
if (item instanceof Element) {
Dependency dependency = new Dependency()
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
Element element = (Element) item
def groupId = element.getElementsByTagName('groupId')
if (groupId.length > 0) {
@@ -186,24 +187,14 @@ class PomAssert {
}
dependencies.put(dependency.generateId(), dependency)
}
}
}
private static String generateId(String groupId, String artifactId) {
groupId + ':' + artifactId
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.groupId = groupId
dependency.artifactId = artifactId
dependency.generateId()
}
private static class Dependency {
String groupId
String artifactId
String version
String generateId() {
generateId(groupId, artifactId)
}
}
}

View File

@@ -43,10 +43,10 @@ class MainControllerIntegrationTests extends AbstractMainControllerIntegrationTe
@Test
void simpleTgzProject() {
downloadTgz('/starter.tgz?style=data-jpa').isJavaProject().isMavenProject()
downloadTgz('/starter.tgz?style=org.acme:bar').isJavaProject().isMavenProject()
.hasStaticAndTemplatesResources(false).pomAssert()
.hasDependenciesCount(2)
.hasSpringBootStarterDependency('data-jpa')
.hasDependency('org.acme', 'bar', '2.1.0')
}
@Test

View File

@@ -6,13 +6,22 @@ info:
initializr:
dependencies:
- name: Core
starters:
content:
- name: Web
value: web
id: web
- name: Security
value: security
id: security
- name: Data JPA
value: data-jpa
id: data-jpa
- name: Other
content:
- name: Foo
groupId: org.acme
artifactId: foo
version: 1.3.5
- name: Bar
id: org.acme:bar
version: 2.1.0
types:
- name: Maven POM
id: pom.xml