Add the ability to refresh the metadata

This commit updates all consumers of the InitializrMetadata to go
through an InitializrMetadataProvider instead of having a direct
handle to the metadata instance.

A default InitializrMetadataProvider implementation that looks up
for some metadata from projects.spring.io has also been added.

That method is actually cached with a TTL of 10 minutes. This
allows the service to update itself automatically when such
metadata are updated in Sagan.

Fixes gh-26
This commit is contained in:
Stephane Nicoll
2014-08-21 16:15:25 +02:00
parent 6e4718fb55
commit fc8d93120c
15 changed files with 370 additions and 31 deletions

View File

@@ -30,6 +30,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
@@ -40,6 +44,10 @@
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-ant</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -77,6 +85,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0</version>
</dependency>
<dependency>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>

View File

@@ -0,0 +1,66 @@
/*
* 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 org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cache.annotation.Cacheable
/**
* A default {@link InitializrMetadataProvider} that is able to refresh
* the metadata with the status of the main spring.io site.
*
* @author Stephane Nicoll
* @since 1.0
*/
class DefaultInitializrMetadataProvider implements InitializrMetadataProvider {
private static final Logger logger = LoggerFactory.getLogger(DefaultInitializrMetadataProvider)
private final InitializrMetadata metadata
@Autowired
DefaultInitializrMetadataProvider(InitializrMetadata metadata) {
this.metadata = metadata
}
@Override
@Cacheable(value = 'initializr', key = "'metadata'")
InitializrMetadata get() {
List<InitializrMetadata.BootVersion> bootVersions = fetchBootVersions()
if (bootVersions != null && !bootVersions.isEmpty()) {
metadata.merge(bootVersions)
}
metadata
}
protected List<InitializrMetadata.BootVersion> fetchBootVersions() {
def url = metadata.env.springBootMetadataUrl
if (url != null) {
try {
logger.info('Fetching boot metadata from '+ url)
return new SpringBootMetadataReader(url).getBootVersions()
} catch (Exception e) {
logger.warn('Failed to fetch spring boot metadata', e)
}
}
null
}
}

View File

@@ -1,9 +1,17 @@
package io.spring.initializr
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.TimeUnit
import com.google.common.cache.CacheBuilder
import io.spring.initializr.web.MainController
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.concurrent.ConcurrentMapCache
import org.springframework.cache.support.SimpleCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -20,18 +28,44 @@ import org.springframework.context.annotation.Configuration
* @since 1.0
*/
@Configuration
@EnableConfigurationProperties(InitializrMetadata.class)
@EnableCaching
@EnableConfigurationProperties(InitializrMetadata)
class InitializrAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MainController.class)
@ConditionalOnMissingBean(MainController)
MainController initializrMainController() {
new MainController()
}
@Bean
@ConditionalOnMissingBean(ProjectGenerator.class)
@ConditionalOnMissingBean(ProjectGenerator)
ProjectGenerator projectGenerator() {
new ProjectGenerator()
}
@Bean
@ConditionalOnMissingBean(InitializrMetadataProvider)
InitializrMetadataProvider initializrMetadataProvider(InitializrMetadata metadata) {
return new DefaultInitializrMetadataProvider(metadata)
}
@Bean
@ConditionalOnMissingBean(CacheManager)
CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager()
cacheManager.setCaches(Arrays.asList(
createConcurrentMapCache(600, 'initializr')
))
cacheManager
}
private static ConcurrentMapCache createConcurrentMapCache(Long timeToLive, String name) {
CacheBuilder<Object, Object> cacheBuilder =
CacheBuilder.newBuilder().expireAfterWrite(timeToLive, TimeUnit.SECONDS);
ConcurrentMap<Object, Object> map = cacheBuilder.build().asMap();
new ConcurrentMapCache(name, map, false);
}
}

View File

@@ -75,6 +75,14 @@ class InitializrMetadata {
return indexedDependencies.get(id)
}
/**
* Create an URL suitable to download Spring Boot cli for the specified version and extension.
*/
String createCliDistributionURl(String extension) {
env.artifactRepository + "org/springframework/boot/spring-boot-cli/" +
"$defaults.bootVersion/spring-boot-cli-$defaults.bootVersion-bin.$extension"
}
/**
* Initializes a {@link ProjectRequest} instance with the defaults
* defined in this instance.
@@ -88,6 +96,19 @@ class InitializrMetadata {
request
}
/**
* Merge this instance with the specified content.
*/
void merge(List<BootVersion> bootVersions) {
if (bootVersions != null) {
synchronized (this.bootVersions) {
this.bootVersions.clear()
this.bootVersions.addAll(bootVersions)
}
}
refreshDefaults()
}
/**
* Initialize and validate the configuration.
*/
@@ -104,6 +125,10 @@ class InitializrMetadata {
}
env.validate()
refreshDefaults()
}
private void refreshDefaults() {
defaults.type = getDefault(types)
defaults.packaging = getDefault(packagings)
defaults.javaVersion = getDefault(javaVersions)
@@ -266,12 +291,7 @@ class InitializrMetadata {
String artifactRepository = 'https://repo.spring.io/release/'
/**
* Create an URL suitable to download Spring Boot cli for the specified version and extension.
*/
String createCliDistributionURl(String version, String extension) {
artifactRepository + "org/springframework/boot/spring-boot-cli/$version/spring-boot-cli-$version-bin.$extension"
}
String springBootMetadataUrl = 'https://spring.io/project_metadata/spring-boot'
void validate() {
if (!artifactRepository.endsWith('/')) {

View File

@@ -0,0 +1,34 @@
/*
* 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
/**
* Provide the {@link InitializrMetadata} to use.
*
* @author Stephane Nicoll
* @since 1.0
*/
interface InitializrMetadataProvider {
/**
* Return the metadata to use. Rather than keeping a handle to
* a particular instance, implementations may decide to refresh
* or recompute the metadata if necessary.
*/
InitializrMetadata get()
}

View File

@@ -0,0 +1,58 @@
/*
* 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 groovy.json.JsonSlurper
import org.springframework.web.client.RestTemplate
/**
* Reads metadata from the main spring.io website. This is a stateful
* service: creates a new instance whenever you need to refresh the
* content.
*
* @author Stephane Nicoll
* @since 1.0
*/
class SpringBootMetadataReader {
private final def content
/**
* Parse the content of the metadata at the specified url
*/
SpringBootMetadataReader(String url) {
RestTemplate restTemplate = new RestTemplate()
String content = restTemplate.getForObject(url, String.class)
this.content = new JsonSlurper().parseText(content)
}
/**
* Return the boot versions parsed by this instance.
*/
List<InitializrMetadata.BootVersion> getBootVersions() {
content.projectReleases.collect {
InitializrMetadata.BootVersion version = new InitializrMetadata.BootVersion()
version.id = it.version
String name = it.versionDisplayName
version.name = (it.snapshot ? name + ' (SNAPSHOT)' : name)
version.setDefault(it.current)
version
}
}
}

View File

@@ -16,14 +16,14 @@
package io.spring.initializr.web
import io.spring.initializr.InitializrMetadata
import io.spring.initializr.InitializrMetadataProvider
import org.springframework.beans.factory.annotation.Autowired
import static io.spring.initializr.support.GroovyTemplate.template
/**
* A base controller that uses {@link InitializrMetadata}
* A base controller that uses a {@link InitializrMetadataProvider}
*
* @author Stephane Nicoll
* @since 1.0
@@ -31,14 +31,14 @@ import static io.spring.initializr.support.GroovyTemplate.template
abstract class AbstractInitializrController {
@Autowired
protected InitializrMetadata metadata
protected InitializrMetadataProvider metadataProvider
/**
* Render the home page with the specified template.
*/
protected String renderHome(String templatePath) {
def model = [:]
metadata.properties.each { model[it.key] = it.value }
metadataProvider.get().properties.each { model[it.key] = it.value }
template templatePath, model
}

View File

@@ -51,14 +51,14 @@ class MainController extends AbstractInitializrController {
@ModelAttribute
ProjectRequest projectRequest() {
ProjectRequest request = new ProjectRequest()
metadata.initializeProjectRequest(request)
metadataProvider.get().initializeProjectRequest(request)
request
}
@RequestMapping(value = "/")
@ResponseBody
InitializrMetadata metadata() {
metadata
metadataProvider.get()
}
@RequestMapping(value = '/', produces = 'text/html')
@@ -69,12 +69,12 @@ class MainController extends AbstractInitializrController {
@RequestMapping('/spring')
String spring() {
'redirect:' + metadata.env.createCliDistributionURl(metadata.defaults.bootVersion, 'zip')
'redirect:' + metadataProvider.get().createCliDistributionURl('zip')
}
@RequestMapping(value = ['/spring.tar.gz', 'spring.tgz'])
String springTgz() {
'redirect:' + metadata.env.createCliDistributionURl(metadata.defaults.bootVersion, 'tar.gz')
'redirect:' + metadataProvider.get().createCliDistributionURl('tar.gz')
}
@RequestMapping('/pom')
@@ -127,6 +127,4 @@ class MainController extends AbstractInitializrController {
result
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.Test
import static org.junit.Assert.*
/**
* @author Stephane Nicoll
*/
class DefaultInitializrMetadataProviderTests {
@Test
void bootVersionsAreReplaced() {
InitializrMetadata metadata = new InitializrMetadataBuilder()
.addBootVersion('0.0.9.RELEASE', true).addBootVersion('0.0.8.RELEASE', false).validateAndGet()
assertEquals '0.0.9.RELEASE', metadata.defaults.bootVersion
DefaultInitializrMetadataProvider provider = new DefaultInitializrMetadataProvider(metadata)
InitializrMetadata updatedMetadata = provider.get()
assertNotNull updatedMetadata.bootVersions
assertFalse 'Boot versions must be set', updatedMetadata.bootVersions.isEmpty()
String defaultVersion = null
updatedMetadata.bootVersions.each {
assertFalse '0.0.9.RELEASE should have been removed', '0.0.9.RELEASE'.equals(it.id)
assertFalse '0.0.8.RELEASE should have been removed', '0.0.8.RELEASE'.equals(it.id)
if (it.default) {
defaultVersion = it.id
}
}
assertNotNull 'A default boot version must be set', defaultVersion
assertEquals 'Default boot version not updated properly', defaultVersion, updatedMetadata.defaults.bootVersion
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 org.junit.Test
import static org.junit.Assert.assertNotNull
import static org.junit.Assert.fail
/**
* @author Stephane Nicoll
*/
class SpringBootMetadataReaderTests {
private final InitializrMetadata metadata = new InitializrMetadata()
@Test
void readAvailableVersions() {
def versions = new SpringBootMetadataReader(metadata.env.springBootMetadataUrl).bootVersions
assertNotNull "spring boot versions should not be null", versions
boolean defaultFound
versions.each {
assertNotNull 'Id must be set', it.id
assertNotNull 'Name must be set', it.name
if (it.default) {
if (defaultFound) {
fail('One default version was already found ' + it.id)
}
defaultFound = true
}
}
}
}

View File

@@ -16,6 +16,9 @@
package io.spring.initializr.web
import io.spring.initializr.DefaultInitializrMetadataProvider
import io.spring.initializr.InitializrMetadata
import io.spring.initializr.InitializrMetadataProvider
import io.spring.initializr.support.ProjectAssert
import org.junit.Rule
import org.junit.rules.TemporaryFolder
@@ -25,6 +28,7 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.IntegrationTest
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
@@ -108,5 +112,17 @@ abstract class AbstractInitializrControllerIntegrationTests {
}
@EnableAutoConfiguration
static class Config {}
static class Config {
@Bean
InitializrMetadataProvider initializrMetadataProvider(InitializrMetadata metadata) {
new DefaultInitializrMetadataProvider(metadata) {
@Override
protected List<InitializrMetadata.BootVersion> fetchBootVersions() {
return null; // Disable metadata fetching from spring.io
}
}
}
}
}

View File

@@ -34,8 +34,8 @@ class MainControllerEnvIntegrationTests extends AbstractInitializrControllerInte
void downloadCliWithCustomRepository() {
HttpEntity entity = restTemplate.getForEntity(createUrl('/spring'), HttpEntity.class)
assertEquals HttpStatus.FOUND, entity.getStatusCode()
assertEquals new URI('https://repo.spring.io/lib-release/org/springframework/boot/spring-boot-cli/1.1.5.RELEASE' +
'/spring-boot-cli-1.1.5.RELEASE-bin.zip'), entity.getHeaders().getLocation()
assertEquals new URI('https://repo.spring.io/lib-release/org/springframework/boot/spring-boot-cli/1.1.4.RELEASE' +
'/spring-boot-cli-1.1.4.RELEASE-bin.zip'), entity.getHeaders().getLocation()
}
}

View File

@@ -85,8 +85,8 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
private void assertSpringCliRedirect(String context, String extension) {
ResponseEntity<?> entity = restTemplate.getForEntity(createUrl(context), ResponseEntity.class)
assertEquals HttpStatus.FOUND, entity.getStatusCode()
assertEquals new URI('https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/1.1.5.RELEASE' +
'/spring-boot-cli-1.1.5.RELEASE-bin.'+extension), entity.getHeaders().getLocation()
assertEquals new URI('https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/1.1.4.RELEASE' +
'/spring-boot-cli-1.1.4.RELEASE-bin.'+extension), entity.getHeaders().getLocation()
}
@@ -136,7 +136,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
void infoHasExternalProperties() {
String body = restTemplate.getForObject(createUrl('/info'), String)
assertTrue('Wrong body:\n' + body, body.contains('"spring-boot"'))
assertTrue('Wrong body:\n' + body, body.contains('"version":"1.1.5.RELEASE"'))
assertTrue('Wrong body:\n' + body, body.contains('"version":"1.1.4.RELEASE"'))
}
@Test

View File

@@ -1,6 +1,6 @@
info:
spring-boot:
version: 1.1.5.RELEASE
version: 1.1.4.RELEASE
initializr:
@@ -68,8 +68,8 @@ initializr:
- name : Latest SNAPSHOT
id: 1.2.0.BUILD-SNAPSHOT
default: false
- name: 1.1.5
id: 1.1.5.RELEASE
- name: 1.1.4
id: 1.1.4.RELEASE
default: true
- name: 1.0.2
id: 1.0.2.RELEASE

View File

@@ -99,8 +99,8 @@
"default": false
},
{
"name": "1.1.5",
"id": "1.1.5.RELEASE",
"name": "1.1.4",
"id": "1.1.4.RELEASE",
"default": true
},
{
@@ -119,6 +119,6 @@
"packaging": "jar",
"javaVersion": "1.7",
"language": "java",
"bootVersion": "1.1.5.RELEASE"
"bootVersion": "1.1.4.RELEASE"
}
}