Fetch spring boot version from api.spring.io

This commit is contained in:
aboyko
2023-09-28 13:58:28 -04:00
parent aaa03e1fa6
commit 7b0aa7faac
10 changed files with 56 additions and 224 deletions

View File

@@ -22,10 +22,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler;
import org.springframework.ide.vscode.boot.java.rewrite.SpringBootUpgrade;
import org.springframework.ide.vscode.boot.validation.BootVersionValidationEngine;
import org.springframework.ide.vscode.boot.validation.generations.BootVersionsFromMavenCentral;
import org.springframework.ide.vscode.boot.validation.generations.CachedBootVersionsFromMavenCentral;
import org.springframework.ide.vscode.boot.validation.generations.GenerationsValidator;
import org.springframework.ide.vscode.boot.validation.generations.ProjectVersionDiagnosticProvider;
import org.springframework.ide.vscode.boot.validation.generations.SpringIoProjectsProvider;
import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider;
import org.springframework.ide.vscode.boot.validation.generations.UpdateBootVersion;
import org.springframework.ide.vscode.boot.validation.generations.VersionValidator;
import org.springframework.ide.vscode.commons.java.IJavaProject;
@@ -38,26 +38,22 @@ public class BootVersionValidationConfig {
private static final Logger log = LoggerFactory.getLogger(BootVersionValidationConfig.class);
@Bean UpdateBootVersion updateBootVersion(SimpleLanguageServer server, Optional<SpringBootUpgrade> bootUpgradeOpt, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
return new UpdateBootVersion(server.getDiagnosticSeverityProvider(), bootUpgradeOpt, cachedVersionsFromMaven);
@Bean UpdateBootVersion updateBootVersion(SimpleLanguageServer server, Optional<SpringBootUpgrade> bootUpgradeOpt, SpringProjectsProvider projectsProvider) {
return new UpdateBootVersion(server.getDiagnosticSeverityProvider(), bootUpgradeOpt, projectsProvider);
}
@Bean GenerationsValidator generationsValidator(SimpleLanguageServer server, BootJavaConfig config, RestTemplateFactory restTemplateFactory, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
return new GenerationsValidator(server.getDiagnosticSeverityProvider(), config, restTemplateFactory, cachedVersionsFromMaven);
@Bean SpringIoProjectsProvider springProjectsProvider(BootJavaConfig config, RestTemplateFactory restTemplateFactory) {
return new SpringIoProjectsProvider(config, restTemplateFactory);
}
@Bean GenerationsValidator generationsValidator(SimpleLanguageServer server, SpringProjectsProvider projectsProvider) {
return new GenerationsValidator(server.getDiagnosticSeverityProvider(), projectsProvider);
}
@Bean ProjectVersionDiagnosticProvider projectVersionDiagnosticProvider(List<VersionValidator> validators) {
return new ProjectVersionDiagnosticProvider(validators);
}
@Bean BootVersionsFromMavenCentral booVersionsFromMavenCentral(RestTemplateFactory restTemplateFactory) {
return new BootVersionsFromMavenCentral(restTemplateFactory);
}
@Bean CachedBootVersionsFromMavenCentral cachedBootVersionsFromMavenCentral(BootVersionsFromMavenCentral bootVersionsFromMaven) {
return new CachedBootVersionsFromMavenCentral(bootVersionsFromMaven);
}
@ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness")
@ConditionalOnProperty(prefix = "languageserver", name = "reconcile-only-opened-docs", havingValue = "false", matchIfMissing = true)
@Bean

View File

@@ -1,91 +0,0 @@
/*******************************************************************************
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.validation.generations;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ide.vscode.boot.app.RestTemplateFactory;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.java.Version;
import org.springframework.web.client.RestTemplate;
public class BootVersionsFromMavenCentral {
private static final Logger log = LoggerFactory.getLogger(BootVersionsFromMavenCentral.class);
private static final URI URL = URI.create("https://search.maven.org/solrsearch/select?q=g:org.springframework.boot+AND+a:spring-boot-starter-parent&core=gav&rows=200&wt=json");
private RestTemplateFactory restTemplateFactory;
public BootVersionsFromMavenCentral(RestTemplateFactory restTemplateFactory) {
this.restTemplateFactory = restTemplateFactory;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public List<Version> getBootVersions() throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(MediaType.parseMediaTypes("application/json"));
HttpEntity<?> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = restTemplateFactory.createRestTemplate(URL.getHost());
log.info("search maven central for Spring Boot release information via: " + URL);
ResponseEntity<Map> responseEntity = restTemplate.exchange(URL, HttpMethod.GET, entity, Map.class);
HttpStatusCode status = responseEntity.getStatusCode();
log.info("search maven central response code: " + status.value());
if (status.is2xxSuccessful()) {
Map<String, Object> json = responseEntity.getBody();
Map<String, Object> response = (Map<String, Object>) json.get("response");
if (response != null) {
List<Version> versions = new ArrayList<>();
Object docs = response.get("docs");
if (docs instanceof List) {
for (Object o : (List<?>) docs) {
if (o instanceof Map) {
Map<String, Object> e = (Map<String, Object>) o;
if (e.get("v") instanceof String) {
try {
versions.add(SpringProjectUtil.getVersion((String) e.get("v")));
} catch (Exception ex) {
// ignore
}
}
}
}
}
Collections.sort(versions);
return versions;
}
else {
throw new IOException("Unable to access Spring Boot versions from Maven Central, empty response");
}
}
else {
throw new IOException("Failed to fetch versions from Maven Central, status = " + status.value());
}
}
}

View File

@@ -1,91 +0,0 @@
/*******************************************************************************
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.validation.generations;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.java.Version;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
public class CachedBootVersionsFromMavenCentral {
private static final Logger log = LoggerFactory.getLogger(CachedBootVersionsFromMavenCentral.class);
private static final String KEY = "cacheKey";
private static final Duration EXPIRES_AFTER = Duration.ofMinutes(60);
private static final int ATTEMPTS_NUMBER = 5;
private static final long RESPONSE_WAIT_TIME_MS = 1000;
private BootVersionsFromMavenCentral bootVersionsFromMaven;
public CachedBootVersionsFromMavenCentral(BootVersionsFromMavenCentral bootVersionsFromMaven) {
this.bootVersionsFromMaven = bootVersionsFromMaven;
}
private final LoadingCache<String, List<Version>> cache = CacheBuilder.newBuilder()
.expireAfterWrite(EXPIRES_AFTER)
.build(new CacheLoader<String, List<Version>>() {
@Override
public List<Version> load(String key) throws Exception {
for (int i = 0; i < ATTEMPTS_NUMBER; i++) {
CompletableFuture<List<Version>> future = null;
try {
future = getFuture();
return future.get(RESPONSE_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
} catch (ExecutionException | TimeoutException e) {
// ignore exception - ask maven central again
if (future != null) {
future.cancel(true);
}
}
}
log.error("Failed to fetch versions from Maven Central after " + ATTEMPTS_NUMBER + " tries.");
return Collections.emptyList();
}
});
public synchronized List<Version> getBootVersions() {
try {
return cache.get(KEY);
}
catch (ExecutionException e) {
log.error("Failed to load Spring Boot release information from maven central", e);
return ImmutableList.of();
}
}
private CompletableFuture<List<Version>> getFuture() {
return CompletableFuture.supplyAsync(() -> {
try {
return bootVersionsFromMaven.getBootVersions();
} catch (IOException e) {
throw new CompletionException(e);
}
});
}
}

View File

@@ -14,8 +14,6 @@ import java.util.Collection;
import java.util.List;
import org.eclipse.lsp4j.Diagnostic;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.app.RestTemplateFactory;
import org.springframework.ide.vscode.boot.validation.generations.json.Generation;
import org.springframework.ide.vscode.boot.validation.generations.json.ResolvedSpringProject;
import org.springframework.ide.vscode.boot.validation.generations.preferences.VersionValidationProblemType;
@@ -29,12 +27,11 @@ import com.google.common.collect.ImmutableList;
public class GenerationsValidator extends AbstractDiagnosticValidator {
private SpringIoProjectsProvider provider;
private SpringProjectsProvider provider;
public GenerationsValidator(DiagnosticSeverityProvider diagnosticSeverityProvider, BootJavaConfig config, RestTemplateFactory restTemplateFactory, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
public GenerationsValidator(DiagnosticSeverityProvider diagnosticSeverityProvider, SpringProjectsProvider provider) {
super(diagnosticSeverityProvider);
provider = new SpringIoProjectsProvider(config.getSpringIOApiUrl(), restTemplateFactory, cachedVersionsFromMaven);
config.addListener(v -> provider.updateIoApiUri(config.getSpringIOApiUrl()));
this.provider = provider;
}
private static Generation getGenerationForJavaProject(IJavaProject javaProject, ResolvedSpringProject springProject)

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.validation.generations;
import java.util.List;
import java.util.Map;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.app.RestTemplateFactory;
import org.springframework.ide.vscode.boot.validation.generations.json.ResolvedSpringProject;
import org.springframework.ide.vscode.boot.validation.generations.json.SpringProject;
@@ -34,11 +35,11 @@ public class SpringIoProjectsProvider implements SpringProjectsProvider {
private SpringProjectsClient client;
private Map<String, ResolvedSpringProject> cache;
private RestTemplateFactory restTemplateFactory;
private CachedBootVersionsFromMavenCentral cachedVersionsFromMaven;
public SpringIoProjectsProvider(String uri, RestTemplateFactory restTemplateFactory, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
public SpringIoProjectsProvider(BootJavaConfig config, RestTemplateFactory restTemplateFactory) {
this.restTemplateFactory = restTemplateFactory;
updateIoApiUri(uri);
updateIoApiUri(config.getSpringIOApiUrl());
config.addListener(v -> updateIoApiUri(config.getSpringIOApiUrl()));
}
public synchronized void updateIoApiUri(String uri) {
@@ -75,7 +76,7 @@ public class SpringIoProjectsProvider implements SpringProjectsProvider {
List<SpringProject> projects = springProjects.getProjects();
if (projects != null) {
for (SpringProject project : projects) {
builder.put(project.getSlug(), new ResolvedSpringProject(project, client, cachedVersionsFromMaven));
builder.put(project.getSlug(), new ResolvedSpringProject(project, client));
}
}
}

View File

@@ -36,17 +36,17 @@ public class UpdateBootVersion extends AbstractDiagnosticValidator {
private Optional<SpringBootUpgrade> bootUpgradeOpt;
private CachedBootVersionsFromMavenCentral cachedVersionsFromMaven;
private SpringProjectsProvider springProjectsProvider;
public UpdateBootVersion(DiagnosticSeverityProvider diagnosticSeverityProvider, Optional<SpringBootUpgrade> bootUpgradeOpt, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
public UpdateBootVersion(DiagnosticSeverityProvider diagnosticSeverityProvider, Optional<SpringBootUpgrade> bootUpgradeOpt, SpringProjectsProvider springProjectsProvider) {
super(diagnosticSeverityProvider);
this.bootUpgradeOpt = bootUpgradeOpt;
this.cachedVersionsFromMaven = cachedVersionsFromMaven;
this.springProjectsProvider = springProjectsProvider;
}
@Override
public Collection<Diagnostic> validate(IJavaProject javaProject, Version javaProjectVersion) throws Exception {
List<Version> versions = cachedVersionsFromMaven.getBootVersions();
List<Version> versions = springProjectsProvider.getProject(SpringProjectUtil.SPRING_BOOT).getReleases();
ImmutableList.Builder<Diagnostic> builder = ImmutableList.builder();
validateMajorVersion(javaProject, javaProjectVersion, versions).ifPresent(builder::add);
validateMinorVersion(javaProject, javaProjectVersion, versions).ifPresent(builder::add);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -15,8 +15,14 @@ import org.springframework.ide.vscode.commons.java.Version;
public class Release extends JsonHalLinks {
public enum Status {
SNAPSHOT,
PRERELEASE,
GENERAL_AVAILABILITY
}
private String version;
private String status;
private Status status;
private boolean current;
private String referenceDocUrl;
private String apiDocUrl;
@@ -26,7 +32,7 @@ public class Release extends JsonHalLinks {
return SpringProjectUtil.getVersion(version);
}
public String getStatus() {
public Status getStatus() {
return status;
}
@@ -41,5 +47,5 @@ public class Release extends JsonHalLinks {
public String getApiDocUrl() {
return apiDocUrl;
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* Copyright (c) 2022,2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -11,8 +11,8 @@
package org.springframework.ide.vscode.boot.validation.generations.json;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.boot.validation.generations.CachedBootVersionsFromMavenCentral;
import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsClient;
import org.springframework.ide.vscode.commons.java.Version;
@@ -22,9 +22,9 @@ public class ResolvedSpringProject extends SpringProject {
private final SpringProjectsClient client;
private Generations generations;
private CachedBootVersionsFromMavenCentral cachedVersionsFromMaven;
private List<Version> releases;
public ResolvedSpringProject(SpringProject project, SpringProjectsClient client, CachedBootVersionsFromMavenCentral cachedVersionsFromMaven) {
public ResolvedSpringProject(SpringProject project, SpringProjectsClient client) {
this.client = client;
setName(project.getName());
setRepositoryUrl(project.getRepositoryUrl());
@@ -53,7 +53,21 @@ public class ResolvedSpringProject extends SpringProject {
* @throws Exception
*/
public List<Version> getReleases() throws Exception {
return cachedVersionsFromMaven.getBootVersions();
// cache the releases to prevent frequent calls to the client
if (this.releases == null) {
Links _links = get_links();
if (_links != null) {
Link genLink = _links.getReleases();
if (genLink != null) {
Releases rs = client.getReleases(genLink.getHref());
releases = rs == null ? null
: rs.getReleases().stream()
.filter(r -> r.getStatus() == Release.Status.GENERAL_AVAILABILITY)
.map(r -> r.getVersion()).collect(Collectors.toList());
}
}
}
return this.releases != null ? releases : ImmutableList.of();
}
}

View File

@@ -22,10 +22,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.app.RestTemplateFactory;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.HoverTestConf;
import org.springframework.ide.vscode.boot.validation.generations.CachedBootVersionsFromMavenCentral;
import org.springframework.ide.vscode.boot.validation.generations.SpringIoProjectsProvider;
import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider;
import org.springframework.ide.vscode.boot.validation.generations.json.Generation;
@@ -45,7 +45,7 @@ public class ProjectGenerationsValidationTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private RestTemplateFactory restTemplateFactory;
@Autowired private CachedBootVersionsFromMavenCentral cachedVersions;
@Autowired private BootJavaConfig config;
private ProjectsHarness projects = ProjectsHarness.INSTANCE;
@@ -59,7 +59,7 @@ public class ProjectGenerationsValidationTest {
@Test
void testProjectsInfoFromSpringIo() throws Exception {
SpringProjectsProvider cache = new SpringIoProjectsProvider("https://api.spring.io/projects", restTemplateFactory, cachedVersions);
SpringProjectsProvider cache = new SpringIoProjectsProvider(config, restTemplateFactory);
SpringProject project = cache.getProject("spring-boot");
assertNotNull(project);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2020 Pivotal, Inc.
* Copyright (c) 2020, 2023 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -34,7 +34,7 @@ public class SampleProjectsProvider implements SpringProjectsProvider {
@Override
public ResolvedSpringProject getProject(String projectSlug) throws Exception {
ResolvedSpringProject project = new ResolvedSpringProject(getSpringProject(projectSlug), null, null) {
ResolvedSpringProject project = new ResolvedSpringProject(getSpringProject(projectSlug), null) {
@Override
public List<Generation> getGenerations() throws Exception {