Skip to content

Commit

Permalink
Merge 865936b into 616d2a2
Browse files Browse the repository at this point in the history
  • Loading branch information
rladdusaw committed Sep 21, 2020
2 parents 616d2a2 + 865936b commit 1ec3826
Show file tree
Hide file tree
Showing 9 changed files with 869 additions and 43 deletions.
3 changes: 2 additions & 1 deletion src/main/java/edu/tamu/app/model/ServiceType.java
Expand Up @@ -8,7 +8,8 @@
public enum ServiceType {

VERSION_ONE("Version One"),
GITHUB("GitHub");
GITHUB_PROJECT("GitHub Project"),
GITHUB_MILESTONE("GitHub Milestone");

private String gloss;

Expand Down
343 changes: 343 additions & 0 deletions src/main/java/edu/tamu/app/service/manager/GitHubMilestoneService.java
@@ -0,0 +1,343 @@
package edu.tamu.app.service.manager;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;
import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHLabel;
import org.kohsuke.github.GHMilestone;
import org.kohsuke.github.GHMilestoneState;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHProject;
import org.kohsuke.github.GHProject.ProjectStateFilter;
import org.kohsuke.github.GHProjectCard;
import org.kohsuke.github.GHProjectColumn;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import edu.tamu.app.cache.model.Card;
import edu.tamu.app.cache.model.Member;
import edu.tamu.app.cache.model.RemoteProject;
import edu.tamu.app.cache.model.Sprint;
import edu.tamu.app.model.ManagementService;
import edu.tamu.app.model.request.FeatureRequest;
import edu.tamu.app.rest.TokenAuthRestTemplate;

public class GitHubMilestoneService extends MappingRemoteProjectManagerBean {

private static final Logger logger = Logger.getLogger(GitHubProjectService.class);

protected static final String ORGANIZATION = "TAMULib";
protected static final String REQUEST_LABEL = "request";
protected static final String ISSUE_LABEL = "issue";
protected static final String FEATURE_LABEL = "feature";
protected static final String DEFECT_LABEL = "bug";
protected static final String SPRINT = "SPRINT";

private final ManagementService managementService;

private final GitHubBuilder ghBuilder;

private final GitHub github;

private final Map<String, Member> members;

private final RestTemplate restTemplate;

private GHLabel label;

public GitHubMilestoneService(final ManagementService managementService) throws IOException {
this.managementService = managementService;
ghBuilder = new GitHubBuilder();
github = getGitHubInstance();
restTemplate = getRestTemplate();
members = new HashMap<String, Member>();
}

@Override
public List<RemoteProject> getRemoteProject() throws Exception {
logger.info("Fetching remote projects");
final List<RemoteProject> remoteProjects = new ArrayList<RemoteProject>();
final GHOrganization org = github.getOrganization(ORGANIZATION);
for (GHRepository repo : org.getRepositories().values()) {
final List<GHLabel> labels = repo.listLabels().asList();
remoteProjects.add(buildRemoteProject(repo, labels));
}
return remoteProjects;
}

@Override
public RemoteProject getRemoteProjectByScopeId(final String scopeId) throws Exception {
logger.info("Fetching remote project by scope id " + scopeId);
GHRepository repo = github.getRepositoryById(scopeId);
List<GHLabel> labels = repo.listLabels().asList();
return buildRemoteProject(repo, labels);
}

@Override
public List<Sprint> getActiveSprintsByScopeId(final String scopeId) throws Exception {
logger.info("Fetching active sprints for remote project with scope id " + scopeId);
List<Sprint> activeSprints = new ArrayList<Sprint>();
GHRepository repo = github.getRepositoryById(scopeId);
List<GHProject> projects = repo.listProjects(ProjectStateFilter.OPEN).asList();
for (GHProject project : projects) {
String sprintId = String.valueOf(project.getId());
String projectName = repo.getName();
Map<String, List<Card>> partitionedCards = getCards(project);
for (Entry<String, List<Card>> partition : partitionedCards.entrySet()) {
activeSprints.add(new Sprint(sprintId, partition.getKey(), projectName, partition.getValue()));
}
}
return activeSprints;
}

@Override
public List<Sprint> getAdditionalActiveSprints() throws Exception {
GHOrganization organization = github.getOrganization(ORGANIZATION);
List<GHProject> projects = organization.listProjects(ProjectStateFilter.OPEN).asList();
List<Sprint> sprints = new ArrayList<Sprint>();
for (GHProject project : projects) {
String sprintId = String.valueOf(project.getId());
Map<String, List<Card>> partitionedCards = getCards(project);
String productName = String.format("%s - %s", organization.getName(), project.getName());
int count = 0;
for (Entry<String, List<Card>> partition : partitionedCards.entrySet()) {
sprints.add(new Sprint(sprintId + "-" + count, partition.getKey(), productName, partition.getValue()));
count++;
}
}
return sprints;
}

@Override
public String push(final FeatureRequest request) throws Exception {
logger.info("Submitting feature request " + request.getTitle() + " to product with scope id " + request.getScopeId());

String scopeId = String.valueOf(request.getScopeId());
String title = request.getTitle();
String body = request.getDescription();
GHRepository repo = github.getRepositoryById(scopeId);

return Long.toString(repo.createIssue(title).body(body).create().getId());
}

protected GitHub getGitHubInstance() throws IOException {
final Optional<String> endpoint = Optional.of(managementService.getUrl());
final Optional<String> token = Optional.of(managementService.getToken());

if (!endpoint.isPresent()) {
throw new RuntimeException("GitHub service endpoint was not defined");
}

if (!token.isPresent()) {
throw new RuntimeException("GitHub token was not defined");
}

return ghBuilder
.withEndpoint(endpoint.get())
.withOAuthToken(token.get())
.build();
}

private RestTemplate getRestTemplate() {
return new TokenAuthRestTemplate(managementService.getToken());
}

private RemoteProject buildRemoteProject(GHRepository repo, List<GHLabel> labels) throws IOException {
final String scopeId = String.valueOf(repo.getId());
final String name = repo.getName();
long requestCount = getPrimaryWorkItemCount(REQUEST_LABEL, repo, labels);
long issueCount = getPrimaryWorkItemCount(ISSUE_LABEL, repo, labels);
long featureCount = getPrimaryWorkItemCount(FEATURE_LABEL, repo, labels);
long defectCount = getPrimaryWorkItemCount(DEFECT_LABEL, repo, labels);

return new RemoteProject(scopeId, name, requestCount, issueCount, featureCount, defectCount, 0L);
}

private long getPrimaryWorkItemCount(final String type, final GHRepository repo, final List<GHLabel> labels)
throws IOException {
label = getLabelByName(labels, type);
if (label == null) {
return 0;
}
return repo.listIssues(GHIssueState.OPEN)
.asList()
.stream()
.filter(this::cardIsLabelType)
.count();
}

private GHLabel getLabelByName(final List<GHLabel> labels, final String name) {
GHLabel returnValue = null;
Optional<GHLabel> match = labels.stream()
.filter(label -> label.getName().equals(name))
.findFirst();
if (match.isPresent()) {
returnValue = match.get();
}
return returnValue;
}

private boolean cardIsLabelType(GHIssue card) {
try {
Collection<GHLabel> labels = card.getLabels();
if (label.getName().equals(ISSUE_LABEL) && isAnIssue(card)) {
return true;
}
return hasLabelByName(labels, label.getName());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private boolean isAnIssue(GHIssue card) throws IOException {
Collection<GHLabel> labels = card.getLabels();
return !hasLabelByName(labels, REQUEST_LABEL)
&& !hasLabelByName(labels, DEFECT_LABEL)
&& !hasLabelByName(labels, FEATURE_LABEL);
}

private boolean hasLabelByName(Collection<GHLabel> labels, String name) {
return labels.parallelStream()
.filter(cardLabel -> cardLabel.getName().equals(name))
.findAny()
.isPresent();
}

private Map<String, List<Card>> getCards(GHProject project) throws IOException {
Map<String, List<Card>> cardsByMilestone = new HashMap<>();
for (GHProjectColumn column : project.listColumns().asList()) {
List<GHProjectCard> projectCards = column.listCards().asList();
Map<Long, GHIssue> cardContents = new HashMap<>();
for (GHProjectCard card : projectCards) {
cardContents.put(card.getId(), card.getContent());
}
Map<GHMilestone, List<GHProjectCard>> partitionedCards = projectCards.stream()
// Card without contents is a note
.filter(c -> cardContents.get(c.getId()) != null)
// Card without a milestone is not on the sprint
.filter(c -> cardContents.get(c.getId()).getMilestone() != null)
.collect(Collectors.groupingBy(c -> cardContents.get(c.getId()).getMilestone()));

for (Entry<GHMilestone, List<GHProjectCard>> partition : partitionedCards.entrySet()) {
List<Card> cards = new ArrayList<>();
for (GHProjectCard card : partition.getValue()) {
GHIssue content = cardContents.get(card.getId());
String id = String.valueOf(card.getId());
String name = content.getTitle();
String number = String.valueOf(content.getNumber());
String type = getCardType(content);
String description = content.getBody();
String status = card.getColumn().getName();
// TODO: Figure out how we want to handle sizes
String estimate = null;
List<Member> assignees = new ArrayList<Member>();
for (GHUser user : content.getAssignees()) {
assignees.add(getMember(user));
}
cards.add(new Card(id, number, mapCardType(type), name, description, mapStatus(status), mapEstimate(estimate), assignees));
}
GHMilestone milestone = partition.getKey();
if (milestone.getState().equals(GHMilestoneState.OPEN) && milestone.getTitle().toUpperCase().contains(SPRINT)) {
String title = partition.getKey().getTitle();
if (cardsByMilestone.containsKey(title)) {
cardsByMilestone.get(title).addAll(cards);
} else {
cardsByMilestone.put(title, cards);
}
}
}
}
return cardsByMilestone;
}

private String getCardType(GHIssue content) throws IOException {
List<GHLabel> labels = (List<GHLabel>) content.getLabels();
GHLabel label = labels.stream()
.filter(label1 -> label1.getName().equals(DEFECT_LABEL))
.findFirst()
.orElseGet(() -> labels.stream()
.filter(label2 -> label2.getName().equals(FEATURE_LABEL))
.findFirst()
.orElseGet(() -> labels.stream()
.filter(label3 -> label3.getName().equals(ISSUE_LABEL))
.findFirst()
.orElseGet(() -> labels.stream()
.filter(label4 -> label4.getName().equals(REQUEST_LABEL))
.findFirst()
.orElse(null)
)
)
);
return label == null ? null : label.getName();
}

protected Member getMember(GHUser user) throws IOException {
Member member;
String memberId = String.valueOf(user.getId());
Optional<Member> cachedMember = getCachedMember(memberId);
if (cachedMember.isPresent()) {
member = cachedMember.get();
} else {
String name = user.getName();
String avatarUrlString = user.getAvatarUrl();
String avatarPath = getAvatarPath(avatarUrlString);
member = new Member(memberId, name, avatarPath);

Optional<URL> avatarUrl = Optional.ofNullable(getClass().getResource("/images/" + avatarPath));
if (!avatarUrl.isPresent()) {
storeAvatar(avatarUrlString);
}

cacheMember(memberId, member);
}
return member;
}

private Optional<Member> getCachedMember(final String id) {
return Optional.ofNullable(members.get(id));
}

private String getAvatarPath(String url) {
return url.substring(url.indexOf("/u/") + 3, url.indexOf("?"));
}

private void storeAvatar(String avatarUrl) throws IOException {
URL imagesPath = getClass().getResource("/images/");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<byte[]> response = restTemplate.exchange(avatarUrl, HttpMethod.GET, entity, byte[].class, "1");
if (response.getStatusCode().equals(HttpStatus.OK)) {
File file = new File(imagesPath.getFile() + getAvatarPath(avatarUrl));
Files.write(file.toPath(), response.getBody());
}
}

private void cacheMember(String id, Member member) {
members.put(id, member);
}

}

0 comments on commit 1ec3826

Please sign in to comment.