Merge pull request #279 from dsyer:feature/url

* pr/279:
  Polish contribution
  Add optional links to a dependency
This commit is contained in:
Stephane Nicoll 2017-01-30 17:26:46 +01:00
commit bf306e5ecb
11 changed files with 471 additions and 7 deletions

View File

@ -102,6 +102,8 @@ class Dependency extends MetadataElement {
List<String> keywords = [] List<String> keywords = []
List<Link> links = []
void setScope(String scope) { void setScope(String scope) {
if (!SCOPE_ALL.contains(scope)) { if (!SCOPE_ALL.contains(scope)) {
throw new InvalidInitializrMetadataException("Invalid scope $scope must be one of $SCOPE_ALL") throw new InvalidInitializrMetadataException("Invalid scope $scope must be one of $SCOPE_ALL")
@ -161,6 +163,9 @@ class Dependency extends MetadataElement {
"Invalid dependency, id should have the form groupId:artifactId[:version] but got $id") "Invalid dependency, id should have the form groupId:artifactId[:version] but got $id")
} }
} }
links.forEach { l ->
l.resolve()
}
updateVersionRanges(VersionParser.DEFAULT) updateVersionRanges(VersionParser.DEFAULT)
} }

View File

@ -0,0 +1,107 @@
/*
* 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 com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import groovy.transform.ToString
/**
* 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 {
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -123,6 +123,15 @@ class DependencyTests {
dependency.resolve() 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 @Test
void generateIdWithNoGroupId() { void generateIdWithNoGroupId() {
def dependency = new Dependency() 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 description: Web dependency description
facets: facets:
- web - web
links:
- rel: guide
href: https://example.com/guide
description: Building a RESTful Web Service
- rel: reference
href: https://example.com/doc
- name: Security - name: Security
id: security id: security
- name: Data JPA - name: Data JPA
@ -55,6 +61,14 @@ initializr:
keywords: keywords:
- thefoo - thefoo
- dafoo - 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 - name: Bar
id: org.acme:bar id: org.acme:bar
version: 2.1.0 version: 2.1.0

View File

@ -52,6 +52,9 @@ class InitializrMetadataV21JsonMapper extends InitializrMetadataV2JsonMapper {
if (dependency.versionRange) { if (dependency.versionRange) {
content['versionRange'] = dependency.versionRange content['versionRange'] = dependency.versionRange
} }
if (dependency.links) {
content._links = LinkMapper.mapLinks(dependency.links)
}
content 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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,9 @@
package io.spring.initializr.web.mapper package io.spring.initializr.web.mapper
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadata import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.metadata.Link
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
import org.junit.Test import org.junit.Test
@ -52,4 +54,17 @@ class InitializrMetadataJsonMapperTests {
result._links.foo.href 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", "groupId": "org.springframework.boot",
"id": "web", "id": "web",
"name": "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, "starter": true,
@ -164,7 +175,23 @@
"starter": true, "starter": true,
"keywords": ["thefoo", "dafoo"], "keywords": ["thefoo", "dafoo"],
"scope": "compile", "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, "starter": true,

View File

@ -30,7 +30,16 @@
{ {
"id": "web", "id": "web",
"name": "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", "id": "security",
@ -47,7 +56,22 @@
"values": [ "values": [
{ {
"id": "org.acme:foo", "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", "id": "org.acme:bar",