Skip to content

Commit

Permalink
Improve error handling
Browse files Browse the repository at this point in the history
This commit also breaks out Github operations for fetch into a new class.
Exceptions that are encountered when loading the cache are ignored so that
cache creation does not fail. Exceptions encountered on writes to Github
are bubbled up and handled using a ControllerAdvice.
  • Loading branch information
mbhave committed Nov 8, 2024
1 parent 79ec204 commit aed4f64
Show file tree
Hide file tree
Showing 14 changed files with 831 additions and 246 deletions.
10 changes: 10 additions & 0 deletions src/main/java/io/spring/projectapi/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.projectapi.ApplicationProperties.Github;
import io.spring.projectapi.github.GithubOperations;
import io.spring.projectapi.github.GithubQueries;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -39,6 +40,15 @@ public GithubOperations githubOperations(RestTemplateBuilder builder, ObjectMapp
return new GithubOperations(builder, objectMapper, accessToken, branch);
}

@Bean
public GithubQueries githubQueries(RestTemplateBuilder builder, ObjectMapper objectMapper,
ApplicationProperties properties) {
Github github = properties.getGithub();
String accessToken = github.getAccesstoken();
String branch = github.getBranch();
return new GithubQueries(builder, objectMapper, accessToken, branch);
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
66 changes: 8 additions & 58 deletions src/main/java/io/spring/projectapi/ProjectRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,80 +17,30 @@
package io.spring.projectapi;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import io.spring.projectapi.github.GithubOperations;
import io.spring.projectapi.github.Project;
import io.spring.projectapi.github.ProjectDocumentation;
import io.spring.projectapi.github.ProjectSupport;
import io.spring.projectapi.web.webhook.CacheController;

import org.springframework.stereotype.Component;

/**
* Caches Github project information. Populated on start up and updates triggered via
* {@link CacheController}.
* Stores project information. Updates triggered via {@link CacheController}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
@Component
public class ProjectRepository {

private final GithubOperations githubOperations;

private transient Data data;

public ProjectRepository(GithubOperations githubOperations) {
this.githubOperations = githubOperations;
this.data = Data.load(githubOperations);
}

public void update() {
this.data = Data.load(this.githubOperations);
}

public Collection<Project> getProjects() {
return this.data.project().values();
}

public Project getProject(String projectSlug) {
return this.data.project().get(projectSlug);
}
public interface ProjectRepository {

public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
return this.data.documentation().get(projectSlug);
}
void update();

public List<ProjectSupport> getProjectSupports(String projectSlug) {
return this.data.support().get(projectSlug);
}
Collection<Project> getProjects();

public String getProjectSupportPolicy(String projectSlug) {
return this.data.supportPolicy().get(projectSlug);
}
Project getProject(String projectSlug);

record Data(Map<String, Project> project, Map<String, List<ProjectDocumentation>> documentation,
Map<String, List<ProjectSupport>> support, Map<String, String> supportPolicy) {
List<ProjectDocumentation> getProjectDocumentations(String projectSlug);

public static Data load(GithubOperations githubOperations) {
Map<String, Project> projects = new LinkedHashMap<>();
Map<String, List<ProjectDocumentation>> documentation = new LinkedHashMap<>();
Map<String, List<ProjectSupport>> support = new LinkedHashMap<>();
Map<String, String> supportPolicy = new LinkedHashMap<>();
githubOperations.getProjects().forEach((project) -> {
String slug = project.getSlug();
projects.put(slug, project);
documentation.put(slug, githubOperations.getProjectDocumentations(slug));
support.put(slug, githubOperations.getProjectSupports(slug));
supportPolicy.put(slug, githubOperations.getProjectSupportPolicy(slug));
});
return new Data(Map.copyOf(projects), Map.copyOf(documentation), Map.copyOf(support),
Map.copyOf(supportPolicy));
}
List<ProjectSupport> getProjectSupports(String projectSlug);

}
String getProjectSupportPolicy(String projectSlug);

}
103 changes: 13 additions & 90 deletions src/main/java/io/spring/projectapi/github/GithubOperations.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import org.slf4j.LoggerFactory;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
Expand All @@ -59,9 +58,6 @@ public class GithubOperations {
private static final TypeReference<@NotNull List<ProjectDocumentation>> DOCUMENTATION_LIST = new TypeReference<>() {
};

private static final TypeReference<List<ProjectSupport>> SUPPORT_LIST = new TypeReference<>() {
};

private static final String GITHUB_URI = "https://api.github.com/repos/spring-io/spring-website-content/contents";

private static final Comparator<ProjectDocumentation> VERSION_COMPARATOR = GithubOperations::compare;
Expand All @@ -78,8 +74,6 @@ public class GithubOperations {

private static final String CONFIG_COMMIT_MESSAGE = "Update Spring Boot Config";

private static final String DEFAULT_SUPPORT_POLICY = "UPSTREAM";

private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};

Expand Down Expand Up @@ -121,11 +115,19 @@ public void addProjectDocumentation(String projectSlug, ProjectDocumentation doc
updateProjectDocumentation(projectSlug, updatedDocumentation, sha);
}

@NotNull
private List<ProjectDocumentation> convertToProjectDocumentation(String content) {
return readValue(content, DOCUMENTATION_LIST);
}

private <T> T readValue(String contents, TypeReference<T> type) {
try {
return this.objectMapper.readValue(contents, type);
}
catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}

private void updateProjectDocumentation(String projectSlug, List<ProjectDocumentation> documentations, String sha) {
try {
byte[] content = this.objectMapper.writer(this.prettyPrinter).writeValueAsBytes(documentations);
Expand All @@ -140,14 +142,14 @@ public void patchProjectDetails(String projectSlug, ProjectDetails projectDetail
throwIfProjectDoesNotExist(projectSlug);
if (projectDetails.getSpringBootConfig() != null) {
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "springBootConfig.md");
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "documentation.json");
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "springBootConfig.md");
String sha = getFileSha(response);
updateContents(projectDetails.getSpringBootConfig().getBytes(), sha, projectSlug, "springBootConfig.md",
CONFIG_COMMIT_MESSAGE);
}
if (projectDetails.getBody() != null) {
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "index.md");
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "documentation.json");
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "index.md");
String contents = getFileContents(response);
String sha = getFileSha(response);
String updatedContent = MarkdownUtils.getUpdatedContent(contents, projectDetails.getBody());
Expand Down Expand Up @@ -218,9 +220,9 @@ private ResponseEntity<Map<String, Object>> getFile(String projectSlug, String f
return this.restTemplate.exchange(request, STRING_OBJECT_MAP);
}
catch (HttpClientErrorException ex) {
logger.info("*** Exception thrown for " + projectSlug + " and file " + fileName + " due to "
+ ex.getMessage() + " with status " + ex.getStatusCode());
HttpStatusCode statusCode = ex.getStatusCode();
logger.debug("Failed to get file " + fileName + " for project " + projectSlug + " due to " + ex.getMessage()
+ " with status " + statusCode);
if (statusCode.value() == 404) {
throwIfProjectDoesNotExist(projectSlug);
return null;
Expand Down Expand Up @@ -253,83 +255,4 @@ private String getFileSha(ResponseEntity<Map<String, Object>> exchange) {
return (String) exchange.getBody().get("sha");
}

@Cacheable("projects")
public List<Project> getProjects() {
List<Project> projects = new ArrayList<>();
try {
RequestEntity<Void> request = RequestEntity.get("/project?ref=" + this.branch).build();
ResponseEntity<List<Map<String, Object>>> exchange = this.restTemplate.exchange(request,
STRING_OBJECT_MAP_LIST);
InvalidGithubResponseException.throwIfInvalid(exchange);
List<Map<String, Object>> body = exchange.getBody();
body.forEach((project) -> {
String projectSlug = (String) project.get("name");
try {
Project fetchedProject = getProject(projectSlug);
if (fetchedProject != null) {
projects.add(fetchedProject);
}
}
catch (Exception ex) {
// Ignore project without an index file
}
});
}
catch (HttpClientErrorException ex) {
// Return empty list
}
return List.copyOf(projects);
}

public Project getProject(String projectSlug) {
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "index.md");
if (response == null) {
return null;
}
String contents = getFileContents(response);
Map<String, String> frontMatter = MarkdownUtils.getFrontMatter(contents);
InvalidGithubProjectIndexException.throwIfInvalid(Objects::nonNull, frontMatter, projectSlug);
frontMatter.put("slug", projectSlug);
return this.objectMapper.convertValue(frontMatter, Project.class);
}

public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "documentation.json");
if (response == null) {
return Collections.emptyList();
}
String content = getFileContents(response);
return List.copyOf(convertToProjectDocumentation(content));
}

public List<ProjectSupport> getProjectSupports(String projectSlug) {
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "support.json");
if (response == null) {
return Collections.emptyList();
}
String contents = getFileContents(response);
return List.copyOf(readValue(contents, SUPPORT_LIST));
}

private <T> T readValue(String contents, TypeReference<T> type) {
try {
return this.objectMapper.readValue(contents, type);
}
catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}

public String getProjectSupportPolicy(String projectSlug) {
ResponseEntity<Map<String, Object>> indexResponse = getFile(projectSlug, "index.md");
if (indexResponse == null) {
return DEFAULT_SUPPORT_POLICY;
}
String indexContents = getFileContents(indexResponse);
Map<String, String> frontMatter = MarkdownUtils.getFrontMatter(indexContents);
InvalidGithubProjectIndexException.throwIfInvalid(Objects::nonNull, frontMatter, projectSlug);
String supportPolicy = frontMatter.get("supportPolicy");
return (supportPolicy != null) ? supportPolicy : DEFAULT_SUPPORT_POLICY;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2022-2023 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
*
* https://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.projectapi.github;

import java.util.Collection;
import java.util.List;

import io.spring.projectapi.ProjectRepository;

import org.springframework.stereotype.Component;

/**
* {@link ProjectRepository} backed by Github.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
@Component
class GithubProjectRepository implements ProjectRepository {

private final GithubQueries githubQueries;

private transient ProjectData projectData;

GithubProjectRepository(GithubQueries githubQueries) {
this.githubQueries = githubQueries;
this.projectData = ProjectData.load(githubQueries);
}

@Override
public void update() {
this.projectData = ProjectData.load(this.githubQueries);
}

@Override
public Collection<Project> getProjects() {
return this.projectData.project().values();
}

@Override
public Project getProject(String projectSlug) {
Project project = this.projectData.project().get(projectSlug);
NoSuchGithubProjectException.throwIfNotFound(project, projectSlug);
return project;
}

@Override
public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
List<ProjectDocumentation> documentations = this.projectData.documentation().get(projectSlug);
NoSuchGithubProjectException.throwIfNotFound(documentations, projectSlug);
return documentations;
}

@Override
public List<ProjectSupport> getProjectSupports(String projectSlug) {
List<ProjectSupport> projectSupports = this.projectData.support().get(projectSlug);
NoSuchGithubProjectException.throwIfNotFound(projectSupports, projectSlug);
return projectSupports;
}

@Override
public String getProjectSupportPolicy(String projectSlug) {
String policy = this.projectData.supportPolicy().get(projectSlug);
NoSuchGithubProjectException.throwIfNotFound(policy, projectSlug);
return policy;
}

}
Loading

0 comments on commit aed4f64

Please sign in to comment.