package edu.ncsu.dlf.servlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DatatypeConverter;
import edu.ncsu.dlf.database.DBAbstraction;
import edu.ncsu.dlf.database.DatabaseFactory;
import edu.ncsu.dlf.model.Pdf;
import edu.ncsu.dlf.model.PdfComment;
import edu.ncsu.dlf.model.PdfComment.Tag;
import edu.ncsu.dlf.model.Repo;
import edu.ncsu.dlf.model.Review;
import edu.ncsu.dlf.utils.ImageUtils;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.eclipse.egit.github.core.Issue;
import org.eclipse.egit.github.core.Label;
import org.eclipse.egit.github.core.Repository;
import org.eclipse.egit.github.core.RepositoryContents;
import org.eclipse.egit.github.core.User;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.service.ContentsService;
import org.eclipse.egit.github.core.service.IssueService;
import org.eclipse.egit.github.core.service.LabelService;
import org.eclipse.egit.github.core.service.RepositoryService;
import org.eclipse.egit.github.core.service.UserService;
import org.json.JSONException;
import org.json.JSONObject;
public class ReviewSubmitServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
final ServletFileUpload upload = new ServletFileUpload();
Repo repo = new Repo(req.getParameter("writer"), req.getParameter("repoName"));
String accessToken = req.getParameter("access_token");
if (repo.repoOwner == null || repo.repoName == null || accessToken == null) {
System.out.println("Something blank");
resp.sendError(500);
return;
}
GitHubClient client = new GitHubClient();
client.setOAuth2Token(accessToken);
UserService userService = new UserService(client);
User reviewer = userService.getUser();
Review fulfilledReview = DatabaseFactory.getDatabase().findReview(reviewer.getLogin(), repo);
if (fulfilledReview == null) {
//either they already uploaded the pdf or it doesn't exist
resp.sendError(409); //409 = conflict
return;
}
UploadIssuesRunnable task = new UploadIssuesRunnable();
String urlToPdfInRepo = "";
Pdf pdf = null;
try {
FileItemIterator iter = upload.getItemIterator(req);
FileItemStream file = iter.next();
pdf = new Pdf(file.openStream(), getServletContext());
int totalIssues = getNumTotalIssues(client, repo); //TODO perhaps involve database to avoid race conditions
List<PdfComment> comments = updatePdfWithNumberedAndColoredAnnotations(pdf, repo, totalIssues);
urlToPdfInRepo = addPdfToRepo(pdf, reviewer, client, repo, accessToken);
task.setter(comments, accessToken, repo, totalIssues, fulfilledReview.customLabels);
} catch (Exception e) {
e.printStackTrace();
resp.sendError(500, "There has been an error uploading your Pdf.");
return;
} finally {
if (pdf != null) {
pdf.close();
}
}
resp.getWriter().write(urlToPdfInRepo);
Thread thread = new Thread(task);
thread.start();
}
private List<PdfComment> updatePdfWithNumberedAndColoredAnnotations(Pdf pdf, Repo repo, int totalIssues) throws IOException {
List<PdfComment> pdfComments = pdf.getPDFComments();
if(!pdfComments.isEmpty()) {
// Set the issue numbers
int issueNumber = totalIssues + 1;
for(PdfComment com : pdfComments) {
if(com.getIssueNumber() == 0) {
com.setIssueNumber(issueNumber++);
}
}
// Update the comments to link to the repository and their newly assigned issue number
pdf.updateCommentsWithColorsAndLinks(pdfComments, repo);
}
return pdfComments;
}
private int getNumTotalIssues(GitHubClient client, Repo repo) throws IOException {
IssueService issueService = new IssueService(client);
Map<String, String> prefs = new HashMap<String, String>();
//By default, only open issues are shown
prefs.put(IssueService.FILTER_STATE, "all");
//get all issues for this repo
List<Issue> issues = issueService.getIssues(repo.repoOwner, repo.repoName, prefs);
return issues.size();
}
static void closeReviewIssue(GitHubClient client, Repo repo, String reviewer, String comment) throws IOException {
IssueService issueService = new IssueService(client);
for(Issue issue : issueService.getIssues(repo.repoOwner, repo.repoName, null)) {
if(issue.getAssignee() != null) {
if(issue.getTitle().startsWith("Reviewer - ") && issue.getAssignee().getLogin().equals(reviewer)) {
issueService.createComment(repo.repoOwner, repo.repoName, issue.getNumber(), comment);
issue.setState("closed");
issueService.editIssue(repo.repoOwner, repo.repoName, issue);
}
}
}
}
private String addPdfToRepo(Pdf pdf, User reviewer, GitHubClient client, Repo repo, String accessToken) throws IOException {
String filePath = "reviews/" + reviewer.getLogin() + ".pdf";
String sha = null;
ContentsService contents = new ContentsService(client);
try {
//list all the files in reviews. We can't just fetch our paper, because it might be
//bigger than 1MB which breaks this API call
List<RepositoryContents> files = contents.getContents(getRepo(client, repo), "reviews/");
for(RepositoryContents file: files) {
if (file.getName().equals(reviewer.getLogin()+".pdf")) {
sha = file.getSha();
}
}
} catch(IOException e) {
e.printStackTrace();
}
HttpPut request = new HttpPut(buildURIForFileUpload(accessToken, repo.repoOwner, repo.repoName, filePath));
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
pdf.getDoc().save(output);
String content = DatatypeConverter.printBase64Binary(output.toByteArray());
JSONObject json = new JSONObject();
if (sha == null) {
//if we are uploading the review for the first time
json.put("message", reviewer.getLogin() + " has submitted their review.");
} else {
//updating review
json.put("message", reviewer.getLogin() + " has updated their review.");
json.put("sha", sha);
}
json.put("path", filePath);
json.put("content", content);
StringEntity entity = new StringEntity(json.toString());
entity.setContentType("application/json");
request.setEntity(entity);
HttpClients.createDefault().execute(request);
} catch(JSONException | COSVisitorException e) {
e.printStackTrace();
} finally {
request.releaseConnection();
}
return filePath;
}
private Repository getRepo(GitHubClient client, Repo repo) throws IOException {
RepositoryService repoService = new RepositoryService(client);
return repoService.getRepository(repo.repoOwner, repo.repoName);
}
private URI buildURIForFileUpload(String accessToken, String writerLogin, String repoName, String filePath) throws IOException {
try {
URIBuilder builder = new URIBuilder("https://api.github.com/repos/" + writerLogin + '/' + repoName + "/contents/" + filePath);
builder.addParameter("access_token", accessToken);
return builder.build();
} catch (URISyntaxException e) {
throw new IOException("Could not build uri", e);
}
}
private final static class UploadIssuesRunnable implements Runnable {
private String accessToken;
private List<PdfComment> comments;
private int issueCount;
private Repo repo;
private List<String> customLabelStrings;
public void setter(List<PdfComment> comments, String accessToken, Repo repo, int issueCount, List<String> customLabels) {
this.comments = comments;
this.accessToken = accessToken;
this.repo = repo;
this.issueCount = issueCount;
this.customLabelStrings = customLabels;
}
@Override
public void run() {
try {
GitHubClient client = new GitHubClient();
client.setOAuth2Token(accessToken);
DBAbstraction database = DatabaseFactory.getDatabase();
UserService userService = new UserService(client);
User reviewer = userService.getUser();
List<Label> customLabels = createCustomLabels(client);
createIssues(client, customLabels);
String closeComment = "@" + reviewer.getLogin() + " has reviewed this paper.";
closeReviewIssue(client, repo, reviewer.getLogin(), closeComment);
database.removeReviewFromDatastore(reviewer.getLogin(), repo);
} catch(IOException e) {
e.printStackTrace();
System.err.println("Error processing Pdf.");
}
}
private List<Label> createCustomLabels(GitHubClient client) {
List<Label> labels = new ArrayList<>();
LabelService labelService = new LabelService(client);
for (String customLabel : customLabelStrings) {
Label newLabel = new Label().setColor(randomColor()).setName(customLabel);
try {
Label alreadyExistingLabel = labelService.getLabel(repo.repoOwner, repo.repoName, customLabel);
labels.add(alreadyExistingLabel);
} catch(IOException e) {
System.out.println("new label " +customLabel + " not found. Going to create it");
try {
newLabel = labelService.createLabel(repo.repoOwner, repo.repoName, newLabel);
} catch (IOException e1) {
e1.printStackTrace(); //this is a bigger problem
}
labels.add(newLabel);
}
}
return labels;
}
private String randomColor() {
StringBuilder sb = new StringBuilder(6);
Random r = new Random();
for(int i = 0; i< 6; i++) {
sb.append("0123456789abcdef".charAt(r.nextInt(16)));
}
return sb.toString();
}
public void createIssues(GitHubClient client, List<Label> customLabels) throws IOException {
for(PdfComment comment : comments) {
System.out.println(comment);
createOrUpdateIssue(client, repo, comment, customLabels);
}
}
public void createOrUpdateIssue(GitHubClient client, Repo repo, PdfComment comment, List<Label> customLabels) throws IOException {
IssueService issueService = new IssueService(client);
// If the issue does not already exist
if(comment.getIssueNumber() > issueCount) {
createIssue(repo, comment, issueService, customLabels);
}
// If the issue already exists, update it
else {
updateIssue(repo, comment, issueService);
}
}
private void createIssue(Repo repo, PdfComment comment, IssueService issueService, List<Label> customLabels) throws IOException {
Issue issue = new Issue();
issue.setTitle(comment.getTitle());
String body = comment.getComment();
try {
String imageURL = ImageUtils.uploadPhoto(comment.getImage());
body = String.format("![snippet](%s)%n%n%s", imageURL, body);
} catch (IOException e) {
e.printStackTrace();
// could not upload image, but carry on anyway
}
issue.setBody(body);
List<Label> newLabels = new ArrayList<>(customLabels);
//add tags to labels
for(Tag tag : comment.getTags()) {
Label label = new Label();
label.setName(tag.name()); // these tags are, by default, the normal grey color.
newLabels.add(label); // User can change these to the severity whey want
}
issue.setLabels(newLabels);
//creates an issue remotely
issue = issueService.createIssue(repo.repoOwner, repo.repoName, issue);
comment.setIssueNumber(issue.getNumber());
}
private void updateIssue(Repo repo, PdfComment comment, IssueService issueService)
throws IOException {
System.out.println("Looking for "+repo.repoOwner+'/'+repo.repoName);
Issue issue = issueService.getIssue(repo.repoOwner, repo.repoName, comment.getIssueNumber());
String issueText = comment.getComment();
if(!issue.getBody().equals(issueText)) {
// makes a comment if the text has changed
issueService.createComment(repo.repoOwner, repo.repoName, comment.getIssueNumber(), issueText);
}
List<Label> existingLabels = issue.getLabels();
List<Label> labels = new ArrayList<>();
for(Tag tag : comment.getTags()) {
Label l = new Label();
l.setName(tag.name());
labels.add(l);
}
boolean shouldUpdateLabels = labels.size() != existingLabels.size();
if(!shouldUpdateLabels) {
for(Label l1 : labels) {
shouldUpdateLabels = true;
for(Label l2 : existingLabels) {
if(l1.getName().equals(l2.getName())) {
shouldUpdateLabels = false;
break;
}
}
if(shouldUpdateLabels)
break;
}
}
if(shouldUpdateLabels) {
issue.setLabels(labels);
issueService.editIssue(repo.repoOwner, repo.repoName, issue);
}
}
}
}