Polish contribution

This commit makes sure that each dependency link is HAL compliant (like
the project types in the metadata). Links are grouped by relation with
well known relations to be defined (i.e. 'how-to', 'reference', 'home'
and so forth).

Each link can be "templated" (in the HAL sense) and only `{bootVersion}`
is supported at the moment. This is useful if a precise documentation
section should reference to the actual Stpring Boot version chosen by the
user.

Closes gh-279
This commit is contained in:
Stephane Nicoll 2017-01-27 09:58:50 +01:00
parent d8a1927a36
commit 981b726a12
13 changed files with 444 additions and 23 deletions

View File

@ -91,8 +91,6 @@ class Dependency extends MetadataElement {
String repository
List<Link> links = []
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
int weight
@ -104,6 +102,8 @@ class Dependency extends MetadataElement {
List<String> keywords = []
List<Link> 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)
}

View File

@ -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}`.
* <p>
* 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<String> templateVariables = []
/**
* A description of the link.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
String description
Set<String> 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<String, String> 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)
}
}

View File

@ -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()

View File

@ -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'])
}
}

View File

@ -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

View File

@ -57,9 +57,6 @@ class DependencyMetadataV21JsonMapper implements DependencyMetadataJsonMapper {
if (dep.repository) {
result.repository = dep.repository
}
if (dep.links) {
result.links = dep.links
}
result
}

View File

@ -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
}

View File

@ -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<Link> links) {
def result = [:]
Map<String, List<Link>> 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
}
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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'
}
}

View File

@ -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,

View File

@ -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",