Skip to content

Commit aed4f64

Browse files
committed
Improve error handling
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.
1 parent 79ec204 commit aed4f64

14 files changed

+831
-246
lines changed

src/main/java/io/spring/projectapi/Application.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.fasterxml.jackson.databind.ObjectMapper;
2020
import io.spring.projectapi.ApplicationProperties.Github;
2121
import io.spring.projectapi.github.GithubOperations;
22+
import io.spring.projectapi.github.GithubQueries;
2223

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

43+
@Bean
44+
public GithubQueries githubQueries(RestTemplateBuilder builder, ObjectMapper objectMapper,
45+
ApplicationProperties properties) {
46+
Github github = properties.getGithub();
47+
String accessToken = github.getAccesstoken();
48+
String branch = github.getBranch();
49+
return new GithubQueries(builder, objectMapper, accessToken, branch);
50+
}
51+
4252
public static void main(String[] args) {
4353
SpringApplication.run(Application.class, args);
4454
}

src/main/java/io/spring/projectapi/ProjectRepository.java

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,80 +17,30 @@
1717
package io.spring.projectapi;
1818

1919
import java.util.Collection;
20-
import java.util.LinkedHashMap;
2120
import java.util.List;
22-
import java.util.Map;
2321

24-
import io.spring.projectapi.github.GithubOperations;
2522
import io.spring.projectapi.github.Project;
2623
import io.spring.projectapi.github.ProjectDocumentation;
2724
import io.spring.projectapi.github.ProjectSupport;
2825
import io.spring.projectapi.web.webhook.CacheController;
2926

30-
import org.springframework.stereotype.Component;
31-
3227
/**
33-
* Caches Github project information. Populated on start up and updates triggered via
34-
* {@link CacheController}.
28+
* Stores project information. Updates triggered via {@link CacheController}.
3529
*
3630
* @author Madhura Bhave
37-
* @author Phillip Webb
3831
*/
39-
@Component
40-
public class ProjectRepository {
41-
42-
private final GithubOperations githubOperations;
43-
44-
private transient Data data;
45-
46-
public ProjectRepository(GithubOperations githubOperations) {
47-
this.githubOperations = githubOperations;
48-
this.data = Data.load(githubOperations);
49-
}
50-
51-
public void update() {
52-
this.data = Data.load(this.githubOperations);
53-
}
54-
55-
public Collection<Project> getProjects() {
56-
return this.data.project().values();
57-
}
58-
59-
public Project getProject(String projectSlug) {
60-
return this.data.project().get(projectSlug);
61-
}
32+
public interface ProjectRepository {
6233

63-
public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
64-
return this.data.documentation().get(projectSlug);
65-
}
34+
void update();
6635

67-
public List<ProjectSupport> getProjectSupports(String projectSlug) {
68-
return this.data.support().get(projectSlug);
69-
}
36+
Collection<Project> getProjects();
7037

71-
public String getProjectSupportPolicy(String projectSlug) {
72-
return this.data.supportPolicy().get(projectSlug);
73-
}
38+
Project getProject(String projectSlug);
7439

75-
record Data(Map<String, Project> project, Map<String, List<ProjectDocumentation>> documentation,
76-
Map<String, List<ProjectSupport>> support, Map<String, String> supportPolicy) {
40+
List<ProjectDocumentation> getProjectDocumentations(String projectSlug);
7741

78-
public static Data load(GithubOperations githubOperations) {
79-
Map<String, Project> projects = new LinkedHashMap<>();
80-
Map<String, List<ProjectDocumentation>> documentation = new LinkedHashMap<>();
81-
Map<String, List<ProjectSupport>> support = new LinkedHashMap<>();
82-
Map<String, String> supportPolicy = new LinkedHashMap<>();
83-
githubOperations.getProjects().forEach((project) -> {
84-
String slug = project.getSlug();
85-
projects.put(slug, project);
86-
documentation.put(slug, githubOperations.getProjectDocumentations(slug));
87-
support.put(slug, githubOperations.getProjectSupports(slug));
88-
supportPolicy.put(slug, githubOperations.getProjectSupportPolicy(slug));
89-
});
90-
return new Data(Map.copyOf(projects), Map.copyOf(documentation), Map.copyOf(support),
91-
Map.copyOf(supportPolicy));
92-
}
42+
List<ProjectSupport> getProjectSupports(String projectSlug);
9343

94-
}
44+
String getProjectSupportPolicy(String projectSlug);
9545

9646
}

src/main/java/io/spring/projectapi/github/GithubOperations.java

Lines changed: 13 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.slf4j.LoggerFactory;
4040

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

62-
private static final TypeReference<List<ProjectSupport>> SUPPORT_LIST = new TypeReference<>() {
63-
};
64-
6561
private static final String GITHUB_URI = "https://api.github.com/repos/spring-io/spring-website-content/contents";
6662

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

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

81-
private static final String DEFAULT_SUPPORT_POLICY = "UPSTREAM";
82-
8377
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
8478
};
8579

@@ -121,11 +115,19 @@ public void addProjectDocumentation(String projectSlug, ProjectDocumentation doc
121115
updateProjectDocumentation(projectSlug, updatedDocumentation, sha);
122116
}
123117

124-
@NotNull
125118
private List<ProjectDocumentation> convertToProjectDocumentation(String content) {
126119
return readValue(content, DOCUMENTATION_LIST);
127120
}
128121

122+
private <T> T readValue(String contents, TypeReference<T> type) {
123+
try {
124+
return this.objectMapper.readValue(contents, type);
125+
}
126+
catch (JsonProcessingException ex) {
127+
throw new RuntimeException(ex);
128+
}
129+
}
130+
129131
private void updateProjectDocumentation(String projectSlug, List<ProjectDocumentation> documentations, String sha) {
130132
try {
131133
byte[] content = this.objectMapper.writer(this.prettyPrinter).writeValueAsBytes(documentations);
@@ -140,14 +142,14 @@ public void patchProjectDetails(String projectSlug, ProjectDetails projectDetail
140142
throwIfProjectDoesNotExist(projectSlug);
141143
if (projectDetails.getSpringBootConfig() != null) {
142144
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "springBootConfig.md");
143-
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "documentation.json");
145+
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "springBootConfig.md");
144146
String sha = getFileSha(response);
145147
updateContents(projectDetails.getSpringBootConfig().getBytes(), sha, projectSlug, "springBootConfig.md",
146148
CONFIG_COMMIT_MESSAGE);
147149
}
148150
if (projectDetails.getBody() != null) {
149151
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "index.md");
150-
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "documentation.json");
152+
NoSuchGithubFileFoundException.throwWhenFileNotFound(response, projectSlug, "index.md");
151153
String contents = getFileContents(response);
152154
String sha = getFileSha(response);
153155
String updatedContent = MarkdownUtils.getUpdatedContent(contents, projectDetails.getBody());
@@ -218,9 +220,9 @@ private ResponseEntity<Map<String, Object>> getFile(String projectSlug, String f
218220
return this.restTemplate.exchange(request, STRING_OBJECT_MAP);
219221
}
220222
catch (HttpClientErrorException ex) {
221-
logger.info("*** Exception thrown for " + projectSlug + " and file " + fileName + " due to "
222-
+ ex.getMessage() + " with status " + ex.getStatusCode());
223223
HttpStatusCode statusCode = ex.getStatusCode();
224+
logger.debug("Failed to get file " + fileName + " for project " + projectSlug + " due to " + ex.getMessage()
225+
+ " with status " + statusCode);
224226
if (statusCode.value() == 404) {
225227
throwIfProjectDoesNotExist(projectSlug);
226228
return null;
@@ -253,83 +255,4 @@ private String getFileSha(ResponseEntity<Map<String, Object>> exchange) {
253255
return (String) exchange.getBody().get("sha");
254256
}
255257

256-
@Cacheable("projects")
257-
public List<Project> getProjects() {
258-
List<Project> projects = new ArrayList<>();
259-
try {
260-
RequestEntity<Void> request = RequestEntity.get("/project?ref=" + this.branch).build();
261-
ResponseEntity<List<Map<String, Object>>> exchange = this.restTemplate.exchange(request,
262-
STRING_OBJECT_MAP_LIST);
263-
InvalidGithubResponseException.throwIfInvalid(exchange);
264-
List<Map<String, Object>> body = exchange.getBody();
265-
body.forEach((project) -> {
266-
String projectSlug = (String) project.get("name");
267-
try {
268-
Project fetchedProject = getProject(projectSlug);
269-
if (fetchedProject != null) {
270-
projects.add(fetchedProject);
271-
}
272-
}
273-
catch (Exception ex) {
274-
// Ignore project without an index file
275-
}
276-
});
277-
}
278-
catch (HttpClientErrorException ex) {
279-
// Return empty list
280-
}
281-
return List.copyOf(projects);
282-
}
283-
284-
public Project getProject(String projectSlug) {
285-
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "index.md");
286-
if (response == null) {
287-
return null;
288-
}
289-
String contents = getFileContents(response);
290-
Map<String, String> frontMatter = MarkdownUtils.getFrontMatter(contents);
291-
InvalidGithubProjectIndexException.throwIfInvalid(Objects::nonNull, frontMatter, projectSlug);
292-
frontMatter.put("slug", projectSlug);
293-
return this.objectMapper.convertValue(frontMatter, Project.class);
294-
}
295-
296-
public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
297-
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "documentation.json");
298-
if (response == null) {
299-
return Collections.emptyList();
300-
}
301-
String content = getFileContents(response);
302-
return List.copyOf(convertToProjectDocumentation(content));
303-
}
304-
305-
public List<ProjectSupport> getProjectSupports(String projectSlug) {
306-
ResponseEntity<Map<String, Object>> response = getFile(projectSlug, "support.json");
307-
if (response == null) {
308-
return Collections.emptyList();
309-
}
310-
String contents = getFileContents(response);
311-
return List.copyOf(readValue(contents, SUPPORT_LIST));
312-
}
313-
314-
private <T> T readValue(String contents, TypeReference<T> type) {
315-
try {
316-
return this.objectMapper.readValue(contents, type);
317-
}
318-
catch (JsonProcessingException ex) {
319-
throw new RuntimeException(ex);
320-
}
321-
}
322-
323-
public String getProjectSupportPolicy(String projectSlug) {
324-
ResponseEntity<Map<String, Object>> indexResponse = getFile(projectSlug, "index.md");
325-
if (indexResponse == null) {
326-
return DEFAULT_SUPPORT_POLICY;
327-
}
328-
String indexContents = getFileContents(indexResponse);
329-
Map<String, String> frontMatter = MarkdownUtils.getFrontMatter(indexContents);
330-
InvalidGithubProjectIndexException.throwIfInvalid(Objects::nonNull, frontMatter, projectSlug);
331-
String supportPolicy = frontMatter.get("supportPolicy");
332-
return (supportPolicy != null) ? supportPolicy : DEFAULT_SUPPORT_POLICY;
333-
}
334-
335258
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2022-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.projectapi.github;
18+
19+
import java.util.Collection;
20+
import java.util.List;
21+
22+
import io.spring.projectapi.ProjectRepository;
23+
24+
import org.springframework.stereotype.Component;
25+
26+
/**
27+
* {@link ProjectRepository} backed by Github.
28+
*
29+
* @author Madhura Bhave
30+
* @author Phillip Webb
31+
*/
32+
@Component
33+
class GithubProjectRepository implements ProjectRepository {
34+
35+
private final GithubQueries githubQueries;
36+
37+
private transient ProjectData projectData;
38+
39+
GithubProjectRepository(GithubQueries githubQueries) {
40+
this.githubQueries = githubQueries;
41+
this.projectData = ProjectData.load(githubQueries);
42+
}
43+
44+
@Override
45+
public void update() {
46+
this.projectData = ProjectData.load(this.githubQueries);
47+
}
48+
49+
@Override
50+
public Collection<Project> getProjects() {
51+
return this.projectData.project().values();
52+
}
53+
54+
@Override
55+
public Project getProject(String projectSlug) {
56+
Project project = this.projectData.project().get(projectSlug);
57+
NoSuchGithubProjectException.throwIfNotFound(project, projectSlug);
58+
return project;
59+
}
60+
61+
@Override
62+
public List<ProjectDocumentation> getProjectDocumentations(String projectSlug) {
63+
List<ProjectDocumentation> documentations = this.projectData.documentation().get(projectSlug);
64+
NoSuchGithubProjectException.throwIfNotFound(documentations, projectSlug);
65+
return documentations;
66+
}
67+
68+
@Override
69+
public List<ProjectSupport> getProjectSupports(String projectSlug) {
70+
List<ProjectSupport> projectSupports = this.projectData.support().get(projectSlug);
71+
NoSuchGithubProjectException.throwIfNotFound(projectSupports, projectSlug);
72+
return projectSupports;
73+
}
74+
75+
@Override
76+
public String getProjectSupportPolicy(String projectSlug) {
77+
String policy = this.projectData.supportPolicy().get(projectSlug);
78+
NoSuchGithubProjectException.throwIfNotFound(policy, projectSlug);
79+
return policy;
80+
}
81+
82+
}

0 commit comments

Comments
 (0)