diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy
index ebcd7e93..9efe1027 100644
--- a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy
+++ b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Dependency.groovy
@@ -91,8 +91,6 @@ class Dependency extends MetadataElement {
String repository
- List links = []
-
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
int weight
@@ -104,6 +102,8 @@ class Dependency extends MetadataElement {
List keywords = []
+ List links = []
+
void setScope(String scope) {
if (!SCOPE_ALL.contains(scope)) {
throw new InvalidInitializrMetadataException("Invalid scope $scope must be one of $SCOPE_ALL")
@@ -163,6 +163,9 @@ class Dependency extends MetadataElement {
"Invalid dependency, id should have the form groupId:artifactId[:version] but got $id")
}
}
+ links.forEach { l ->
+ l.resolve()
+ }
updateVersionRanges(VersionParser.DEFAULT)
}
diff --git a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Link.groovy b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Link.groovy
index afa54bc3..ed9ebdc1 100644
--- a/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Link.groovy
+++ b/initializr-generator/src/main/groovy/io/spring/initializr/metadata/Link.groovy
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2015 the original author or authors.
+ * Copyright 2012-2017 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.
@@ -16,16 +16,92 @@
package io.spring.initializr.metadata
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonInclude
+import groovy.transform.ToString
+
/**
- * @author Dave Syer
+ * Metadata for a link. Each link has a "relation" that potentially attaches a strong
+ * semantic to the nature of the link. The URI of the link itself can be templated by
+ * including variables in the form `{variableName}`.
+ *
+ * An actual {@code URI} can be generated using {@code expand}, providing a mapping for
+ * those variables.
*
+ * @author Dave Syer
+ * @author Stephane Nicoll
*/
+@ToString(ignoreNulls = true, includePackage = false)
class Link {
-
- String id
-
- URL url
-
+
+ private static final String VARIABLE_REGEX = "\\{(\\w+)\\}";
+
+ /**
+ * The relation of the link.
+ */
+ String rel;
+
+ /**
+ * The URI the link is pointing to.
+ */
+ String href
+
+ /**
+ * Specify if the URI is templated.
+ */
+ @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+ boolean templated
+
+ @JsonIgnore
+ final Set templateVariables = []
+
+ /**
+ * A description of the link.
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
String description
+ Set getTemplateVariables() {
+ Collections.unmodifiableSet(templateVariables)
+ }
+
+ void setHref(String href) {
+ this.href = href
+ }
+
+ void resolve() {
+ if (!rel) {
+ throw new InvalidInitializrMetadataException(
+ "Invalid link $this: rel attribute is mandatory")
+ }
+ if (!href) {
+ throw new InvalidInitializrMetadataException(
+ "Invalid link $this: href attribute is mandatory")
+ }
+ def matcher = (href =~ VARIABLE_REGEX)
+ while (matcher.find()) {
+ def variable = matcher.group(1)
+ this.templateVariables << variable
+ }
+ this.templated = this.templateVariables
+ }
+
+ /**
+ * Expand the link using the specified parameters.
+ * @param parameters the parameters value
+ * @return an URI where all variables have been expanded
+ */
+ URI expand(Map parameters) {
+ String result = href
+ templateVariables.forEach { var ->
+ Object value = parameters[var]
+ if (!value) {
+ throw new IllegalArgumentException(
+ "Could not explan $href, missing value for '$var'")
+ }
+ result = result.replace("{$var}", value.toString())
+ }
+ new URI(result)
+ }
+
}
diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy
index fc794c34..ee521a11 100644
--- a/initializr-generator/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy
+++ b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/DependencyTests.groovy
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2016 the original author or authors.
+ * Copyright 2012-2017 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.
@@ -123,6 +123,15 @@ class DependencyTests {
dependency.resolve()
}
+ @Test
+ void invalidLink() {
+ def dependency = new Dependency(id : 'foo')
+ dependency.links << new Link(href: 'https://example.com')
+
+ thrown.expect(InvalidInitializrMetadataException)
+ dependency.resolve()
+ }
+
@Test
void generateIdWithNoGroupId() {
def dependency = new Dependency()
diff --git a/initializr-generator/src/test/groovy/io/spring/initializr/metadata/LinkTests.groovy b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/LinkTests.groovy
new file mode 100644
index 00000000..58afbfe2
--- /dev/null
+++ b/initializr-generator/src/test/groovy/io/spring/initializr/metadata/LinkTests.groovy
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2012-2017 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 org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+/**
+ * Tests for {@link Link}.
+ *
+ * @author Stephane Nicoll
+ */
+class LinkTests {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none()
+
+ @Test
+ void resolveInvalidLinkNoRel() {
+ def link = new Link(href: 'https://example.com')
+ thrown.expect(InvalidInitializrMetadataException)
+ link.resolve()
+ }
+
+ @Test
+ void resolveInvalidLinkNoHref() {
+ def link = new Link(rel: 'reference', description: 'foo doc')
+
+ thrown.expect(InvalidInitializrMetadataException)
+ link.resolve()
+ }
+
+ @Test
+ void resolveLinkNoVariables() {
+ def link = new Link(rel: 'reference', href: 'https://example.com/2')
+ link.resolve()
+ assert !link.templated
+ assert link.templateVariables.size() == 0
+ }
+
+ @Test
+ void resolveLinkWithVariables() {
+ def link = new Link(rel: 'reference', href: 'https://example.com/{a}/2/{b}')
+ link.resolve()
+ assert link.templated
+ assert link.templateVariables.size() == 2
+ assert link.templateVariables.contains('a')
+ assert link.templateVariables.contains('b')
+ }
+
+ @Test
+ void expandLink() {
+ def link = new Link(rel: 'reference', href: 'https://example.com/{a}/2/{b}')
+ link.resolve()
+ assert link.expand(['a': 'test', 'b': 'another']) ==
+ new URI('https://example.com/test/2/another')
+ }
+
+ @Test
+ void expandLinkWithSameAttributeAtTwoPlaces() {
+ def link = new Link(rel: 'reference', href: 'https://example.com/{a}/2/{a}')
+ link.resolve()
+ assert link.expand(['a': 'test', 'b': 'another']) ==
+ new URI('https://example.com/test/2/test')
+ }
+
+ @Test
+ void expandLinkMissingVariable() {
+ def link = new Link(rel: 'reference', href: 'https://example.com/{a}/2/{b}')
+ link.resolve()
+
+ thrown.expect(IllegalArgumentException)
+ thrown.expectMessage("missing value for 'b'")
+ link.expand(['a': 'test'])
+ }
+
+}
diff --git a/initializr-generator/src/test/resources/application-test-default.yml b/initializr-generator/src/test/resources/application-test-default.yml
index 320384a3..dae8c3e0 100644
--- a/initializr-generator/src/test/resources/application-test-default.yml
+++ b/initializr-generator/src/test/resources/application-test-default.yml
@@ -39,6 +39,12 @@ initializr:
description: Web dependency description
facets:
- web
+ links:
+ - rel: guide
+ href: https://example.com/guide
+ description: Building a RESTful Web Service
+ - rel: reference
+ href: https://example.com/doc
- name: Security
id: security
- name: Data JPA
@@ -55,6 +61,14 @@ initializr:
keywords:
- thefoo
- dafoo
+ links:
+ - rel: guide
+ href: https://example.com/guide1
+ - rel: reference
+ href: https://example.com/{bootVersion}/doc
+ - rel: guide
+ href: https://example.com/guide2
+ description: Some guide for foo
- name: Bar
id: org.acme:bar
version: 2.1.0
diff --git a/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/DependencyMetadataV21JsonMapper.groovy b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/DependencyMetadataV21JsonMapper.groovy
index a1049b1f..afc0b09d 100644
--- a/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/DependencyMetadataV21JsonMapper.groovy
+++ b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/DependencyMetadataV21JsonMapper.groovy
@@ -57,9 +57,6 @@ class DependencyMetadataV21JsonMapper implements DependencyMetadataJsonMapper {
if (dep.repository) {
result.repository = dep.repository
}
- if (dep.links) {
- result.links = dep.links
- }
result
}
diff --git a/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.groovy b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.groovy
index 618adc4b..fa40d9b8 100644
--- a/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.groovy
+++ b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/InitializrMetadataV21JsonMapper.groovy
@@ -53,7 +53,7 @@ class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMapper {
content['versionRange'] = dependency.versionRange
}
if (dependency.links) {
- content.links = dependency.links
+ content._links = LinkMapper.mapLinks(dependency.links)
}
content
}
diff --git a/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/LinkMapper.groovy b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/LinkMapper.groovy
new file mode 100644
index 00000000..535690fd
--- /dev/null
+++ b/initializr-web/src/main/groovy/io/spring/initializr/web/mapper/LinkMapper.groovy
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012-2017 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.mapper
+
+import io.spring.initializr.metadata.Link
+
+/**
+ * Generate a json representation for {@link Link}
+ *
+ * @author Stephane Nicoll
+ */
+class LinkMapper {
+
+ /**
+ * Map the specified links to a json model. If several links share
+ * the same relation, they are grouped together.
+ * @param links the links to map
+ * @return a model for the specified links
+ */
+ static mapLinks(List links) {
+ def result = [:]
+ Map> byRel = new LinkedHashMap<>()
+ links.each {
+ def relLinks = byRel[it.rel]
+ if (!relLinks) {
+ relLinks = []
+ byRel[it.rel] = relLinks
+ }
+ relLinks.add(it)
+ }
+ byRel.forEach { rel, l ->
+ if (l.size() == 1) {
+ def root = [:]
+ mapLink(l[0], root)
+ result[rel] = root
+ } else {
+ def root = []
+ l.each {
+ def model = [:]
+ mapLink(it, model)
+ root << model
+ }
+ result[rel] = root
+ }
+ }
+ result
+ }
+
+ private static mapLink(Link link, def model) {
+ model.href = link.href
+ if (link.templated) {
+ model.templated = true
+ }
+ if (link.description) {
+ model.title = link.description
+ }
+ }
+
+}
diff --git a/initializr-web/src/main/groovy/io/spring/initializr/web/ui/UiController.groovy b/initializr-web/src/main/groovy/io/spring/initializr/web/ui/UiController.groovy
index ff5f3c9f..4bc97cc4 100644
--- a/initializr-web/src/main/groovy/io/spring/initializr/web/ui/UiController.groovy
+++ b/initializr-web/src/main/groovy/io/spring/initializr/web/ui/UiController.groovy
@@ -82,10 +82,6 @@ class UiController {
if (d.description) {
result.description = d.description
}
- if (d.links) {
- result.url = d.links[0].url
- result.links = d.links
- }
if (d.weight) {
result.weight = d.weight
}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.groovy
index a20f2f6f..8e9eaba4 100644
--- a/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.groovy
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/InitializrMetadataJsonMapperTests.groovy
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2015 the original author or authors.
+ * Copyright 2012-2017 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.
@@ -17,7 +17,9 @@
package io.spring.initializr.web.mapper
import groovy.json.JsonSlurper
+import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadata
+import io.spring.initializr.metadata.Link
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
import org.junit.Test
@@ -52,4 +54,17 @@ class InitializrMetadataJsonMapperTests {
result._links.foo.href
}
+ @Test
+ void keepLinksOrdering() {
+ def dependency = new Dependency(id: 'foo')
+ dependency.links << new Link(rel: 'guide', href: 'https://example.com/how-to')
+ dependency.links << new Link(rel: 'reference', href: 'https://example.com/doc')
+ InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults()
+ .addDependencyGroup('test', dependency).build()
+ def json = jsonMapper.write(metadata, null)
+ def first = json.indexOf('https://example.com/how-to')
+ def second = json.indexOf('https://example.com/doc')
+ assert first < second
+ }
+
}
diff --git a/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/LinkMapperTests.groovy b/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/LinkMapperTests.groovy
new file mode 100644
index 00000000..f57ed38d
--- /dev/null
+++ b/initializr-web/src/test/groovy/io/spring/initializr/web/mapper/LinkMapperTests.groovy
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012-2017 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.mapper
+
+import io.spring.initializr.metadata.Link
+import org.junit.Test
+
+/**
+ * Tests for {@link LinkMapper}.
+ *
+ * @author Stephane Nicoll
+ */
+class LinkMapperTests {
+
+ @Test
+ void mapSimpleRel() {
+ def links = new ArrayList()
+ links << new Link(rel: 'a', 'href': 'https://example.com',
+ description: 'some description')
+ def model = LinkMapper.mapLinks(links)
+ assert model.size() == 1
+ assert model.containsKey('a')
+ def linkModel = model['a']
+ assert linkModel.size() == 2
+ assert linkModel['href'] == 'https://example.com'
+ assert linkModel['title'] == 'some description'
+ }
+
+ @Test
+ void mapTemplatedRel() {
+ def links = new ArrayList()
+ links << new Link(rel: 'a', 'href': 'https://example.com/{bootVersion}/a',
+ templated: true)
+ def model = LinkMapper.mapLinks(links)
+ assert model.size() == 1
+ assert model.containsKey('a')
+ def linkModel = model['a']
+ assert linkModel.size() == 2
+ assert linkModel['href'] == 'https://example.com/{bootVersion}/a'
+ assert linkModel['templated'] == true
+ }
+
+ @Test
+ void mergeSeveralLinksInArray() {
+ def links = new ArrayList()
+ links << new Link(rel: 'a', 'href': 'https://example.com',
+ description: 'some description')
+ links << new Link(rel: 'a', 'href': 'https://example.com/2')
+ def model = LinkMapper.mapLinks(links)
+ assert model.size() == 1
+ assert model.containsKey('a')
+ def linksModel = model['a']
+ assert linksModel.size() == 2
+ assert linksModel[0]['href'] == 'https://example.com'
+ assert linksModel[1]['href'] == 'https://example.com/2'
+ }
+
+ @Test
+ void keepOrdering() {
+ def links = new ArrayList()
+ links << new Link(rel: 'a', 'href': 'https://example.com')
+ links << new Link(rel: 'b', 'href': 'https://example.com')
+ def model = LinkMapper.mapLinks(links)
+ def iterator = model.keySet().iterator()
+ assert ++iterator == 'a'
+ assert ++iterator == 'b'
+ }
+
+ @Test
+ void keepOrderingWithMultipleUrlForSameRel() {
+ def links = new ArrayList()
+ links << new Link(rel: 'a', 'href': 'https://example.com')
+ links << new Link(rel: 'b', 'href': 'https://example.com')
+ links << new Link(rel: 'a', 'href': 'https://example.com')
+ def model = LinkMapper.mapLinks(links)
+ def iterator = model.keySet().iterator()
+ assert ++iterator == 'a'
+ assert ++iterator == 'b'
+ }
+
+}
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 87e26f23..4df1a07c 100644
--- a/initializr-web/src/test/resources/metadata/config/test-default.json
+++ b/initializr-web/src/test/resources/metadata/config/test-default.json
@@ -131,7 +131,18 @@
"groupId": "org.springframework.boot",
"id": "web",
"name": "Web",
- "scope": "compile"
+ "scope": "compile",
+ "links": [
+ {
+ "rel": "guide",
+ "description": "Building a RESTful Web Service",
+ "href": "https://example.com/guide"
+ },
+ {
+ "rel": "reference",
+ "href": "https://example.com/doc"
+ }
+ ]
},
{
"starter": true,
@@ -164,7 +175,23 @@
"starter": true,
"keywords": ["thefoo", "dafoo"],
"scope": "compile",
- "version": "1.3.5"
+ "version": "1.3.5",
+ "links": [
+ {
+ "rel": "guide",
+ "href": "https://example.com/guide1"
+ },
+ {
+ "rel": "reference",
+ "href": "https://example.com/{bootVersion}/doc",
+ "templated": true
+ },
+ {
+ "rel": "guide",
+ "description": "Some guide for foo",
+ "href": "https://example.com/guide2"
+ }
+ ]
},
{
"starter": true,
diff --git a/initializr-web/src/test/resources/metadata/test-default-2.1.0.json b/initializr-web/src/test/resources/metadata/test-default-2.1.0.json
index a64a5e2a..65269f38 100644
--- a/initializr-web/src/test/resources/metadata/test-default-2.1.0.json
+++ b/initializr-web/src/test/resources/metadata/test-default-2.1.0.json
@@ -30,7 +30,16 @@
{
"id": "web",
"name": "Web",
- "description": "Web dependency description"
+ "description": "Web dependency description",
+ "_links": {
+ "guide": {
+ "href": "https://example.com/guide",
+ "title": "Building a RESTful Web Service"
+ },
+ "reference": {
+ "href": "https://example.com/doc"
+ }
+ }
},
{
"id": "security",
@@ -47,7 +56,22 @@
"values": [
{
"id": "org.acme:foo",
- "name": "Foo"
+ "name": "Foo",
+ "_links": {
+ "guide": [
+ {
+ "href": "https://example.com/guide1"
+ },
+ {
+ "href": "https://example.com/guide2",
+ "title": "Some guide for foo"
+ }
+ ],
+ "reference": {
+ "href": "https://example.com/{bootVersion}/doc",
+ "templated": true
+ }
+ }
},
{
"id": "org.acme:bar",