Initial commit

This commit is contained in:
Andy Wilkinson
2015-11-30 16:13:06 +00:00
commit 214fec38be
56 changed files with 6314 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2015 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.issuebot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Main class for launching Issue Bot.
*
* @author Andy Wilkinson
*/
@SpringBootApplication
public class IssueBotApplication {
public static void main(String[] args) {
SpringApplication.run(IssueBotApplication.class, args);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2015 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.issuebot.triage;
import io.spring.issuebot.triage.github.GitHubOperations;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@link TriageListener} that applies a label to any issues that require triage.
*
* @author Andy Wilkinson
*/
public class LabelApplyingTriageListener implements TriageListener {
private final GitHubOperations gitHub;
/**
* Creates a new {@code LabelApplyingTriageListener} that will use the given
* {@code gitHubOperations} to apply a label to any issues that require triage.
*
* @param gitHubOperations the GitHubOperations
*/
public LabelApplyingTriageListener(GitHubOperations gitHubOperations) {
this.gitHub = gitHubOperations;
}
@Override
public void requiresTriage(Issue issue, MonitoredRepository repository) {
this.gitHub.addLabel(issue, repository.getLabel());
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 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.issuebot.triage;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
/**
* A repository that should be monitored.
*
* @author Andy Wilkinson
*/
@Getter
@Setter
public class MonitoredRepository {
/**
* The name of the organization that owns the repository.
*/
private String organization;
/**
* The name of the repository.
*/
private String name;
/**
* The names of the project collaborators whose issues do not require triage.
*/
private List<String> collaborators;
/**
* The name of the label that should be applied to issues that are waiting for triage.
*/
private String label;
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2015 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.issuebot.triage;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import io.spring.issuebot.triage.filter.TriageFilter;
import io.spring.issuebot.triage.filter.TriageFilters;
import io.spring.issuebot.triage.github.GitHubOperations;
import io.spring.issuebot.triage.github.Issue;
import io.spring.issuebot.triage.github.Page;
/**
* Central class for monitoring the configured repositories and labeling issues as waiting
* for triage.
*
* @author Andy Wilkinson
*/
class RepositoryMonitor {
private static final Logger log = LoggerFactory.getLogger(RepositoryMonitor.class);
private final List<MonitoredRepository> repositoryConfigurations;
private final TriageFilters filters;
private final GitHubOperations gitHub;
private final TriageListener listener;
RepositoryMonitor(GitHubOperations gitHub, TriageFilters filters,
TriageListener listener, List<MonitoredRepository> repositoryConfigurations) {
this.gitHub = gitHub;
this.filters = filters;
this.listener = listener;
this.repositoryConfigurations = repositoryConfigurations;
}
@Scheduled(fixedRate = 5 * 60 * 1000)
void monitor() {
for (MonitoredRepository configuration : this.repositoryConfigurations) {
monitor(configuration);
}
}
private void monitor(MonitoredRepository repository) {
log.info("Monitoring {}/{}", repository.getOrganization(), repository.getName());
TriageFilter filter = this.filters.filterForRepository(repository);
Page<Issue> page = this.gitHub.getIssues(repository.getOrganization(),
repository.getName());
while (page != null) {
for (Issue issue : page.getContent()) {
if (!filter.triaged(issue)) {
this.listener.requiresTriage(issue, repository);
}
}
page = page.next();
}
log.info("Monitoring of {}/{} completed", repository.getOrganization(),
repository.getName());
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2015 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.issuebot.triage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import io.spring.issuebot.triage.filter.StandardTriageFilters;
import io.spring.issuebot.triage.github.GitHubTemplate;
import io.spring.issuebot.triage.github.RegexLinkParser;
/**
* Central configuration for the beans involved in identifying issues that require triage.
*
* @author Andy Wilkinson
*/
@Configuration
@EnableScheduling
class TriageConfiguration {
@Bean
TriageProperties triageProperties() {
return new TriageProperties();
}
@Bean
LabelApplyingTriageListener triageListener() {
return new LabelApplyingTriageListener(gitHubTemplate());
}
@Bean
RepositoryMonitor repositoryMonitor() {
return new RepositoryMonitor(gitHubTemplate(), triageFilters(), triageListener(),
triageProperties().getRepositories());
}
@Bean
GitHubTemplate gitHubTemplate() {
TriageProperties triageProperties = triageProperties();
return new GitHubTemplate(triageProperties.getUsername(),
triageProperties.getPassword(), linkParser());
}
@Bean
StandardTriageFilters triageFilters() {
return new StandardTriageFilters(gitHubTemplate());
}
@Bean
RegexLinkParser linkParser() {
return new RegexLinkParser();
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2015 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.issuebot.triage;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@code TriageListener} is notified of issues that require triage.
*
* @author Andy Wilkinson
*/
public interface TriageListener {
/**
* Notification that the given {@code issue} requires triage.
*
* @param issue the issue
* @param repository the monitored repository to which the issue belongs
*/
void requiresTriage(Issue issue, MonitoredRepository repository);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2015 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.issuebot.triage;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* {@link EnableConfigurationProperties Configuration properties} for triaging GitHub
* issues.
*
* @author Andy Wilkinson
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "issuebot.triage")
public class TriageProperties {
/**
* Repositories that will be monitored.
*/
private List<MonitoredRepository> repositories;
/**
* GitHub username.
*/
private String username;
/**
* GitHub password.
*/
private String password;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.spring.issuebot.triage.github.Comment;
import io.spring.issuebot.triage.github.GitHubOperations;
import io.spring.issuebot.triage.github.Issue;
import io.spring.issuebot.triage.github.Page;
/**
* A {@code TriageFilter} that considers an issue has having been triaged if a
* collaborator has commented on it.
*
* @author Andy Wilkinson
*/
final class CommentedByCollaboratorTriageFilter implements TriageFilter {
private static final Logger log = LoggerFactory
.getLogger(CommentedByCollaboratorTriageFilter.class);
private final List<String> collaborators;
private final GitHubOperations gitHub;
CommentedByCollaboratorTriageFilter(List<String> collaborators,
GitHubOperations gitHub) {
this.collaborators = collaborators == null ? Collections.emptyList()
: collaborators;
this.gitHub = gitHub;
}
@Override
public boolean triaged(Issue issue) {
Page<Comment> page = this.gitHub.getComments(issue);
while (page != null) {
for (Comment comment : page.getContent()) {
if (this.collaborators.contains(comment.getUser().getLogin())) {
log.debug("{} has been triaged. It was commented on by {}",
issue.getUrl(), comment.getUser().getLogin());
return true;
}
}
page = page.next();
}
return false;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@link TriageFilter} that delegates to one or more filters.
*
* @author Andy Wilkinson
*/
final class DelegatingTriageFilter implements TriageFilter {
private static final Logger log = LoggerFactory
.getLogger(DelegatingTriageFilter.class);
private final List<TriageFilter> filters;
DelegatingTriageFilter(List<TriageFilter> filters) {
this.filters = filters;
}
@Override
public boolean triaged(Issue issue) {
for (TriageFilter filter : this.filters) {
if (filter.triaged(issue)) {
return true;
}
}
log.info("{} is waiting for triage", issue.getUrl());
return false;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@link TriageFilter} that considers an issue as having been triaged if any labels
* have been applied to it.
*
* @author Andy Wilkinson
*/
final class LabelledTriageFilter implements TriageFilter {
private static final Logger log = LoggerFactory.getLogger(LabelledTriageFilter.class);
@Override
public boolean triaged(Issue issue) {
if (issue.getLabels() != null && !issue.getLabels().isEmpty()) {
log.debug("{} has been triaged. It has been labelled.", issue.getUrl());
return true;
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@link TriageFilter} that considers an issue as having been triaged if a milestone
* has been applied to it.
*
* @author Andy Wilkinson
*/
public class MilestoneAppliedTriageFilter implements TriageFilter {
private static final Logger log = LoggerFactory.getLogger(LabelledTriageFilter.class);
@Override
public boolean triaged(Issue issue) {
if (issue.getMilestone() != null) {
log.debug("Issue has been triaged. It has been added to milestone {}",
issue.getMilestone().getTitle());
return true;
}
return false;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@link TriageFilter} that considers an issue as having been triaged if it was opened
* by a collaborator.
*
* @author Andy Wilkinson
*/
final class OpenedByCollaboratorTriageFilter implements TriageFilter {
private static final Logger log = LoggerFactory
.getLogger(OpenedByCollaboratorTriageFilter.class);
private final List<String> collaborators;
OpenedByCollaboratorTriageFilter(List<String> collaborators) {
this.collaborators = collaborators == null ? Collections.emptyList()
: collaborators;
}
@Override
public boolean triaged(Issue issue) {
if (this.collaborators.contains(issue.getUser().getLogin())) {
log.debug("{} has been triaged. It was opened by {}", issue.getUrl(),
issue.getUser().getLogin());
return true;
}
return false;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import java.util.Arrays;
import io.spring.issuebot.triage.MonitoredRepository;
import io.spring.issuebot.triage.github.GitHubOperations;
/**
* Standard implementation of {@code TriageFilters}.
*
* @author Andy Wilkinson
*/
public class StandardTriageFilters implements TriageFilters {
private final GitHubOperations gitHub;
/**
* Creates a new {@code StandardTriageFilters} that will use the given
* {@code gitHubOperations} to interact with GitHub.
*
* @param gitHubOperations the GitHubOperations
*/
public StandardTriageFilters(GitHubOperations gitHubOperations) {
this.gitHub = gitHubOperations;
}
@Override
public TriageFilter filterForRepository(MonitoredRepository repository) {
return new DelegatingTriageFilter(Arrays.asList(
new OpenedByCollaboratorTriageFilter(repository.getCollaborators()),
new LabelledTriageFilter(), new MilestoneAppliedTriageFilter(),
new CommentedByCollaboratorTriageFilter(repository.getCollaborators(),
this.gitHub)));
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import io.spring.issuebot.triage.github.Issue;
/**
* A {@code TriageFilter} is used to identify issues which are waiting for triage.
*
* @author Andy Wilkinson
*/
public interface TriageFilter {
/**
* Returns {@code true} if the given issue has already been triaged, otherwise
* {@code false}.
*
* @param issue the issue
* @return {@code true} if the issue has been triaged, {@code false} otherwise
*/
boolean triaged(Issue issue);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2015 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.issuebot.triage.filter;
import io.spring.issuebot.triage.MonitoredRepository;
/**
* {@code TriageFilters} is used to retrieve a {@code TriageFilter} for a particular
* {@link MonitoredRepository repository}.
*
* @author Andy Wilkinson
*/
public interface TriageFilters {
/**
* Returns the {@code TriageFilter} that should be used to process issues for the
* given {@code repository}.
*
* @param repository the repository
* @return the filter for the repository
*/
TriageFilter filterForRepository(MonitoredRepository repository);
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* A comment that has been made on a GitHub issue.
*
* @author Andy Wilkinson
*/
@Getter
public final class Comment {
private final User user;
/**
* Creates a new comment that was authored by the given {@code user}.
*
* @param user the user
*/
@JsonCreator
public Comment(@JsonProperty("user") User user) {
this.user = user;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 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.issuebot.triage.github;
/**
* Operations that can be performed against the GitHub API.
*
* @author Andy Wilkinson
*/
public interface GitHubOperations {
/**
* Returns the open issues in the {@code repository} owned by the given
* {@code organization}.
*
* @param organization the name of the organization
* @param repository the name of the repository
* @return the issues
*/
Page<Issue> getIssues(String organization, String repository);
/**
* Returns the comments that have been made on the given {@code issue}.
*
* @param issue the issue
* @return the comments
*/
Page<Comment> getComments(Issue issue);
/**
* Adds the given {@code label} to the given {@code issue}.
*
* @param issue the issue
* @param label the label;
* @return the updated issue
*/
Issue addLabel(Issue issue, String label);
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Central class for interacting with GitHub's REST API.
*
* @author Andy Wilkinson
*/
public class GitHubTemplate implements GitHubOperations {
private static final Logger log = LoggerFactory.getLogger(GitHubTemplate.class);
private final RestOperations rest;
private final LinkParser linkParser;
/**
* Creates a new {@code GitHubTemplate} that will use the given {@code username} and
* {@code password} to authenticate, and the given {@code linkParser} to parse links
* from responses' {@code Link} header.
*
* @param username the username
* @param password the password
* @param linkParser the link parser
*/
public GitHubTemplate(String username, String password, LinkParser linkParser) {
this(createDefaultRestTemplate(username, password), linkParser);
}
GitHubTemplate(RestOperations rest, LinkParser linkParser) {
this.rest = rest;
this.linkParser = linkParser;
}
RestOperations getRestOperations() {
return this.rest;
}
static RestTemplate createDefaultRestTemplate(String username, String password) {
RestTemplate rest = new RestTemplate();
rest.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getStatusCode() == HttpStatus.FORBIDDEN && response
.getHeaders().getFirst("X-RateLimit-Remaining").equals("0")) {
throw new IllegalStateException(
"Rate limit exceeded. Limit will reset at "
+ new Date(Long
.valueOf(response.getHeaders()
.getFirst("X-RateLimit-Reset"))
* 1000));
}
}
});
rest.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
rest.setInterceptors(Collections
.singletonList(new BasicAuthorizationInterceptor(username, password)));
return rest;
}
@Override
public Page<Issue> getIssues(String organization, String repository) {
String url = "https://api.github.com/repos/" + organization + "/" + repository
+ "/issues";
return getIssues(url);
}
private Page<Issue> getIssues(String url) {
if (!StringUtils.hasText(url)) {
return null;
}
ResponseEntity<Issue[]> issues = this.rest.getForEntity(url, Issue[].class);
return new StandardPage<Issue>(Arrays.asList(issues.getBody()),
() -> getIssues(getNextUrl(issues)));
}
private String getNextUrl(ResponseEntity<?> response) {
return this.linkParser.parse(response.getHeaders().getFirst("Link")).get("next");
}
@Override
public Page<Comment> getComments(Issue issue) {
return getComments(issue.getCommentsUrl());
}
private Page<Comment> getComments(String url) {
if (!StringUtils.hasText(url)) {
return null;
}
ResponseEntity<Comment[]> comments = this.rest.getForEntity(url, Comment[].class);
return new StandardPage<Comment>(Arrays.asList(comments.getBody()),
() -> getComments(getNextUrl(comments)));
}
@Override
public Issue addLabel(Issue issue, String labelName) {
Map<String, Object> body = new HashMap<>();
body.put("labels", Arrays.asList(labelName));
ResponseEntity<Issue> exchange = this.rest.exchange(
new RequestEntity<>(body, HttpMethod.POST, URI.create(issue.getUrl())),
Issue.class);
if (exchange.getStatusCode() != HttpStatus.OK) {
log.warn("Failed to add label to issue. Response status: "
+ exchange.getStatusCode());
}
return exchange.getBody();
}
private static class BasicAuthorizationInterceptor
implements ClientHttpRequestInterceptor {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final String username;
private final String password;
BasicAuthorizationInterceptor(String username, String password) {
this.username = username;
this.password = (password == null ? "" : password);
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String token = Base64Utils.encodeToString(
(this.username + ":" + this.password).getBytes(UTF_8));
request.getHeaders().add("Authorization", "Basic " + token);
return execution.execute(request, body);
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* A GitHub issue.
*
* @author Andy Wilkinson
*/
@Getter
public class Issue {
private final String url;
private final String commentsUrl;
private final User user;
private final List<Label> labels;
private final Milestone milestone;
/**
* Creates a new {@code Issue}.
*
* @param url the url of the issue in the GitHub API
* @param commentsUrl the url of the comments on the issue in the GitHub API
* @param user the user that created the issue
* @param labels the labels applied to the issue
* @param milestone the milestone applied to the issue
*/
@JsonCreator
public Issue(@JsonProperty("url") String url,
@JsonProperty("comments_url") String commentsUrl,
@JsonProperty("user") User user, @JsonProperty("labels") List<Label> labels,
@JsonProperty("milestone") Milestone milestone) {
this.url = url;
this.commentsUrl = commentsUrl;
this.user = user;
this.labels = labels;
this.milestone = milestone;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* A label that can be applied to a GitHub issue.
*
* @author Andy Wilkinson
*/
@Getter
public class Label {
private final String name;
/**
* Creates a new label with the given {@code name}.
*
* @param name the name of the label
*/
@JsonCreator
public Label(@JsonProperty("name") String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.util.Map;
/**
* A {@code LinkParser} can be used to parse the
* <a href="https://developer.github.com/v3/#link-header">{@code Link} header</a> that is
* returned by the GitHub API.
*
* @author Andy Wilkinson
*/
interface LinkParser {
/**
* Parse the given {@code header} into a map of rel:url pairs.
*
* @param header the header to parse
* @return the map of links
*/
Map<String, String> parse(String header);
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* A milestone to which a GitHub Issue can be added.
*
* @author Andy Wilkinson
*/
@Getter
public class Milestone {
private final String title;
/**
* Creates a new {@code Milestone} with the given {@code title}.
*
* @param title the title
*/
@JsonCreator
public Milestone(@JsonProperty("title") String title) {
this.title = title;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.util.List;
/**
* A page of results.
*
* @param <T> the type of the contents of the page
* @author Andy Wilkinson
*/
public interface Page<T> {
/**
* Returns the next page, if any.
*
* @return The next page or {@code null}
*/
Page<T> next();
/**
* Returns the contents of the page.
*
* @return the contents
*/
List<T> getContent();
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* A {@code LinkParser} that uses a regular expression to parse the header.
*
* @author Andy Wilkinson
*/
public class RegexLinkParser implements LinkParser {
private static final Pattern LINK_PATTERN = Pattern.compile("<(.+)>; rel=\"(.+)\"");
@Override
public Map<String, String> parse(String input) {
Map<String, String> links = new HashMap<>();
for (String link : StringUtils.commaDelimitedListToStringArray(input)) {
Matcher matcher = LINK_PATTERN.matcher(link.trim());
if (matcher.matches()) {
links.put(matcher.group(2), matcher.group(1));
}
}
return links;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import java.util.List;
import java.util.function.Supplier;
/**
* Standard implementation of {@link Page}.
*
* @param <T> the type of the contents of the page
* @author Andy Wilkinson
*/
public class StandardPage<T> implements Page<T> {
private List<T> content;
private Supplier<Page<T>> nextSupplier;
/**
* Creates a new {@code StandardPage} that has the given {@code content}. The given
* {@code nextSupplier} will be used to obtain the next page {@link #next when
* requested}.
*
* @param content the content
* @param nextSupplier the supplier of the next page
*/
public StandardPage(List<T> content, Supplier<Page<T>> nextSupplier) {
this.content = content;
this.nextSupplier = nextSupplier;
}
@Override
public Page<T> next() {
return this.nextSupplier.get();
}
@Override
public List<T> getContent() {
return this.content;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2015 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.issuebot.triage.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* A GitHub user.
*
* @author Andy Wilkinson
*/
@Getter
public class User {
private final String login;
/**
* Creates a new {@code User} with the given login.
*
* @param login the login
*/
@JsonCreator
public User(@JsonProperty("login") String login) {
this.login = login;
}
}