diff --git a/src/main/java/nl/andrewlalis/Main.java b/src/main/java/nl/andrewlalis/Main.java index ad2a75b..f614faa 100644 --- a/src/main/java/nl/andrewlalis/Main.java +++ b/src/main/java/nl/andrewlalis/Main.java @@ -31,21 +31,23 @@ public class Main { // Command executor which will be used by all actions the user can do. CommandExecutor executor = new CommandExecutor(); - // Main application model. - Organization organization = new Organization(); + // Main application model is stored as a static variable that is accessible everywhere. + InitializerApp.organization = new Organization(); // Initialize User Interface. - InitializerApp app = new InitializerApp(executor, organization); + InitializerApp app = new InitializerApp(executor); app.begin(); app.setAccessToken(userOptions.get("token")); // Initialize executable commands. - executor.registerCommand("read_students", new ReadStudentsFile(organization)); + executor.registerCommand("read_students", new ReadStudentsFile(InitializerApp.organization)); executor.registerCommand("archive_all", new ArchiveRepos()); executor.registerCommand("generate_assignments", new GenerateAssignmentsRepo()); executor.registerCommand("define_ta_teams", new DefineTaTeams(app)); - logger.info("GithubManager for Github Repositories in Educational Organizations. Program initialized."); + logger.info("GithubManager for Github Repositories in Educational Organizations.\n" + + "© Andrew Lalis (2018), All rights reserved.\n" + + "Program initialized."); } } diff --git a/src/main/java/nl/andrewlalis/git_api/GithubManager.java b/src/main/java/nl/andrewlalis/git_api/GithubManager.java index 5829129..48db804 100644 --- a/src/main/java/nl/andrewlalis/git_api/GithubManager.java +++ b/src/main/java/nl/andrewlalis/git_api/GithubManager.java @@ -2,7 +2,13 @@ package nl.andrewlalis.git_api; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import nl.andrewlalis.model.*; +import nl.andrewlalis.model.Student; +import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.model.TATeam; +import nl.andrewlalis.model.TeachingAssistant; +import nl.andrewlalis.model.error.Error; +import nl.andrewlalis.model.error.Severity; +import nl.andrewlalis.ui.view.InitializerApp; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPatch; import org.apache.http.entity.StringEntity; @@ -81,7 +87,7 @@ public class GithubManager { public List getMembers() { List teachingAssistants = new ArrayList<>(); try { - for (GHUser member : this.organization.getMembers()) { + for (GHUser member : this.organization.listMembers().asList()) { teachingAssistants.add(new TeachingAssistant(-1, member.getName(), member.getEmail(), member.getLogin())); } } catch (IOException e) { @@ -91,34 +97,6 @@ public class GithubManager { return teachingAssistants; } - /** - * Initializes the github repository for all studentTeams given. - * - * Creates for the entire organization: - * - an assignments repository with protected master branch and TA permissions. - * Creates for each team: - * - a repository - * - protected master branch - * - development branch - * - adds students to repository - * - adds all students to assignments repository. - * @param studentTeams The list of student studentTeams. - * @param teamAll The team of all teaching assistants. - * @param assignmentsRepoName The name of the assignments repo. - * @throws Exception If an error occurs while initializing the github repositories. - */ - public void initializeGithubRepos(List studentTeams, TATeam teamAll, String assignmentsRepoName) throws Exception { - this.setupAssignmentsRepo(assignmentsRepoName, "fuck the police", teamAll.getName()); - - StudentTeam t = new StudentTeam(); - Student s = new Student(3050831, "Andrew Lalis", "andrewlalisofficial@gmail.com", "andrewlalis", null); - t.addMember(s); - t.setId(42); - - this.setupStudentTeam(t, teamAll.getGithubTeam(), "advoop_2018"); - // TODO: Finish this method. - } - /** * Sets up the organization's assignments repository, and grants permissions to all teaching assistants. * @param assignmentsRepoName The name of the assignments repository. @@ -131,6 +109,7 @@ public class GithubManager { // Check if the repository already exists. GHRepository existingRepo = this.organization.getRepository(assignmentsRepoName); if (existingRepo != null) { + InitializerApp.organization.addError(new Error(Severity.MINOR, "Assignments repository already existed, deleting it.")); existingRepo.delete(); logger.fine("Deleted pre-existing assignments repository."); } @@ -155,46 +134,45 @@ public class GithubManager { * @param team The student team to set up. * @param taTeam The team of teaching assistants that is responsible for these students. * @param prefix The prefix to append to the front of the repo name. - * @throws IOException If an HTTP request fails. */ - public void setupStudentTeam(StudentTeam team, GHTeam taTeam, String prefix) throws IOException { + public void setupStudentTeam(StudentTeam team, TATeam taTeam, String prefix) { // First check that the assignments repo exists, otherwise no invitations can be sent. if (this.assignmentsRepo == null) { logger.warning("Assignments repository must be created before student repositories."); return; } - GHRepository repo = this.createRepository(team.generateUniqueName(prefix), taTeam, team.generateRepoDescription(), false, true, false); + GHRepository repo = this.createRepository(team.generateUniqueName(prefix), taTeam.getGithubTeam(), team.generateRepoDescription(), false, true, false); if (repo == null) { logger.severe("Repository for student team " + team.getId() + " could not be created."); return; } - this.protectMasterBranch(repo, taTeam); + this.protectMasterBranch(repo, taTeam.getGithubTeam()); this.createDevelopmentBranch(repo); + this.addTATeamAsAdmin(repo, taTeam.getGithubTeam()); + this.inviteStudentsToRepos(team, repo); - taTeam.add(repo, GHOrganization.Permission.ADMIN); - logger.fine("Added team " + taTeam.getName() + " as admin to repository: " + repo.getName()); - - List users = new ArrayList<>(); - for (Student student : team.getStudents()) { - GHUser user = this.github.getUser(student.getGithubUsername()); - users.add(user); - } - - repo.addCollaborators(users); - this.assignmentsRepo.addCollaborators(users); + team.setRepository(repo); + team.setTaTeam(taTeam); } /** * Deletes all repositories in the organization. - * @throws IOException if an error occurs with sending requests. + * @param substring The substring which repository names should contain to be deleted. */ - public void deleteAllRepositories() throws IOException { + public void deleteAllRepositories(String substring) { List repositories = this.organization.listRepositories().asList(); for (GHRepository repo : repositories) { - repo.delete(); + if (repo.getName().contains(substring)) { + try { + repo.delete(); + } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Could not delete repository: " + repo.getName())); + e.printStackTrace(); + } + } } } @@ -202,7 +180,7 @@ public class GithubManager { * Archives all repositories whose name contains the given substring. * @param sub Any repository containing this substring will be archived. */ - public void archiveAllRepositories(String sub) throws IOException { + public void archiveAllRepositories(String sub) { List repositories = this.organization.listRepositories().asList(); for (GHRepository repo : repositories) { if (repo.getName().contains(sub)) { @@ -215,21 +193,61 @@ public class GithubManager { * Archives a repository so that it can no longer be manipulated. * TODO: Change to using Github API instead of Apache HttpUtils. * @param repo The repository to archive. - * @throws IOException If an error occurs with the HTTP request. */ - public void archiveRepository(GHRepository repo) throws IOException { - HttpPatch patch = new HttpPatch("https://api.github.com/repos/" + repo.getFullName() + "?access_token=" + this.accessToken); - CloseableHttpClient client = HttpClientBuilder.create().build(); - ObjectMapper mapper = new ObjectMapper(); - ObjectNode root = mapper.createObjectNode(); - root.put("archived", true); - String json = mapper.writeValueAsString(root); - patch.setEntity(new StringEntity(json)); - HttpResponse response = client.execute(patch); - if (response.getStatusLine().getStatusCode() != 200) { - throw new IOException("Could not archive repository: " + repo.getName() + ". Code: " + response.getStatusLine().getStatusCode()); + private void archiveRepository(GHRepository repo) { + try { + HttpPatch patch = new HttpPatch("https://api.github.com/repos/" + repo.getFullName() + "?access_token=" + this.accessToken); + CloseableHttpClient client = HttpClientBuilder.create().build(); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode root = mapper.createObjectNode(); + root.put("archived", true); + String json = mapper.writeValueAsString(root); + patch.setEntity(new StringEntity(json)); + HttpResponse response = client.execute(patch); + if (response.getStatusLine().getStatusCode() != 200) { + throw new IOException("Could not archive repository: " + repo.getName() + ". Code: " + response.getStatusLine().getStatusCode()); + } + logger.info("Archived repository: " + repo.getFullName()); + } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Could not archive repository: " + repo.getName())); + } + } + + /** + * Invites students in a team to their repository, and the assignments repository. + * @param team The team of students to invite as collaborators. + * @param repo The repository created for the students. + */ + private void inviteStudentsToRepos(StudentTeam team, GHRepository repo) { + try { + logger.finest("Adding students from team: " + team.getId() + " as collaborators."); + List users = new ArrayList<>(); + for (Student student : team.getStudents()) { + GHUser user = this.github.getUser(student.getGithubUsername()); + users.add(user); + } + + repo.addCollaborators(users); + this.assignmentsRepo.addCollaborators(users); + } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Students in team: " + team + " could not be added as collaborators to assignments repo or their repository.")); + logger.warning("Could not add students as collaborators to assignments or their repo."); + } + } + + /** + * Adds a teaching assistant team as admins to a particular student repository. + * @param studentRepo The student repository. + * @param taTeam The team to give admin rights. + */ + private void addTATeamAsAdmin(GHRepository studentRepo, GHTeam taTeam) { + try { + taTeam.add(studentRepo, GHOrganization.Permission.ADMIN); + logger.fine("Added team " + taTeam.getName() + " as admin to repository: " + studentRepo.getName()); + } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Could not add TA Team: " + taTeam.getName() + " as ADMIN to repository: " + studentRepo.getName())); + logger.severe("Could not add TA Team: " + taTeam.getName() + " as admins to repository: " + studentRepo.getName()); } - logger.info("Archived repository: " + repo.getFullName()); } /** @@ -248,6 +266,7 @@ public class GithubManager { protectionBuilder.enable(); logger.fine("Protected master branch of repository: " + repo.getName()); } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Could not protect master branch of repository: " + repo.getName())); logger.severe("Could not protect master branch of repository: " + repo.getName()); e.printStackTrace(); } @@ -263,6 +282,7 @@ public class GithubManager { repo.createRef("refs/heads/development", sha1); logger.fine("Created development branch of repository: " + repo.getName()); } catch (IOException e) { + InitializerApp.organization.addError(new Error(Severity.HIGH, "Could not create development branch of repository: " + repo.getName())); logger.severe("Could not create development branch for repository: " + repo.getName() + '\n' + e.getMessage()); e.printStackTrace(); } @@ -276,7 +296,7 @@ public class GithubManager { * @param hasWiki Whether the repo has a wiki enabled. * @param hasIssues Whether the repo has issues enabled. * @param isPrivate Whether or not the repository is private. - * @return The repository that was created, or + * @return The repository that was created, or null if it could not be created. */ private GHRepository createRepository(String name, GHTeam taTeam, String description, boolean hasWiki, boolean hasIssues, boolean isPrivate){ try { @@ -286,12 +306,13 @@ public class GithubManager { builder.issues(hasIssues); builder.description(description); builder.gitignoreTemplate("Java"); - builder.private_(isPrivate); // TODO: Change this to true for production + builder.private_(isPrivate); GHRepository repo = builder.create(); logger.fine("Created repository: " + repo.getName()); return repo; } catch (IOException e) { logger.severe("Could not create repository: " + name + '\n' + e.getMessage()); + InitializerApp.organization.addError(new Error(Severity.CRITICAL, "Could not create repository: " + name)); e.printStackTrace(); return null; } diff --git a/src/main/java/nl/andrewlalis/model/Organization.java b/src/main/java/nl/andrewlalis/model/Organization.java index 65d7a0a..8a5df40 100644 --- a/src/main/java/nl/andrewlalis/model/Organization.java +++ b/src/main/java/nl/andrewlalis/model/Organization.java @@ -1,16 +1,17 @@ package nl.andrewlalis.model; -import com.sun.org.apache.xpath.internal.operations.Or; +import nl.andrewlalis.model.error.Error; import java.util.ArrayList; import java.util.List; +import java.util.Observable; /** * This class represents the overarching model container for the entire application, and holds in memory all student * teams created, teaching assistant teams, and any other state information that would be needed by the user interface * or runtime executables. */ -public class Organization { +public class Organization extends Observable { /** * A list of all student teams in this organization. This is generated from a CSV file supplied after many students @@ -18,11 +19,18 @@ public class Organization { */ private List studentTeams; + /** + * A queue of errors that accumulates as the program runs. These will be output to the user after execution of + * critical sections, so that inevitable errors due to input imperfections are not overlooked. + */ + private List errors; + /** * Constructs a new Organization object with all instance variables initialized to empty values. */ public Organization() { this.studentTeams = new ArrayList<>(); + this.errors = new ArrayList<>(); } /** @@ -38,9 +46,25 @@ public class Organization { return this.studentTeams; } + public List getErrors() { + return this.errors; + } + // SETTERS public void setStudentTeams(List teams) { this.studentTeams = teams; + this.setChanged(); + this.notifyObservers(); + } + + /** + * Adds an error to the list of accumulated errors. + * @param newError The newly generated error to add. + */ + public void addError(Error newError) { + this.errors.add(newError); + this.setChanged(); + this.notifyObservers(); } } diff --git a/src/main/java/nl/andrewlalis/model/error/Error.java b/src/main/java/nl/andrewlalis/model/error/Error.java new file mode 100644 index 0000000..eade31c --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/error/Error.java @@ -0,0 +1,45 @@ +package nl.andrewlalis.model.error; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Represents an error that occurs, but does not stop the application while running. Different from an exception in that + * errors are meant to be added to a list and processed at a later time, and not hinder the operations in the present. + */ +public class Error { + + /** + * The time at which this error occurred. + */ + private long timestamp; + + /** + * The severity of the error. + */ + private Severity severity; + + /** + * A custom error message generated at the time of the error. + */ + private String message; + + /** + * Constructs a new Error with a severity and message. + * @param severity The severity of the message. + * @param message The custom generated error message for this error. + */ + public Error(Severity severity, String message) { + this.timestamp = System.currentTimeMillis(); + this.severity = severity; + this.message = message; + } + + @Override + public String toString() { + DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + String dateString = df.format(new Date(this.timestamp)); + return dateString + ' ' + this.severity.toString() + ": " + this.message; + } +} diff --git a/src/main/java/nl/andrewlalis/model/error/Severity.java b/src/main/java/nl/andrewlalis/model/error/Severity.java new file mode 100644 index 0000000..2ab20cf --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/error/Severity.java @@ -0,0 +1,12 @@ +package nl.andrewlalis.model.error; + +/** + * Represents the different levels of severity for errors. + */ +public enum Severity { + CRITICAL, // Anything which happens and could have serious side-effects or massive implications system-wide. + HIGH, // Errors which should be at the top of priority to sort out after a procedure is executed. + MEDIUM, // Medium errors are, as the name implies, of a medium priority. + LOW, // Low severity errors are the smallest errors that should be processed soon to avoid issues. + MINOR // Minor errors are so insignificant that they may not even require user intervention. +} diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java index 41f7975..cec7f12 100644 --- a/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java @@ -2,6 +2,10 @@ package nl.andrewlalis.ui.control.command.executables; import nl.andrewlalis.git_api.GithubManager; +/** + * An executable which opens up a dialog to allow a user to delegate how many student teams each TATeam gets to manage, + * and actually generate the student repositories using the manager. + */ public class GenerateStudentRepos extends GithubExecutable { @Override diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFile.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFile.java index f275885..5ca9656 100644 --- a/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFile.java +++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFile.java @@ -2,7 +2,10 @@ package nl.andrewlalis.ui.control.command.executables; import nl.andrewlalis.model.Organization; import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.model.error.Error; +import nl.andrewlalis.model.error.Severity; import nl.andrewlalis.ui.control.command.Executable; +import nl.andrewlalis.ui.view.InitializerApp; import nl.andrewlalis.util.FileUtils; import java.util.List; @@ -17,15 +20,6 @@ import java.util.List; */ public class ReadStudentsFile implements Executable { - /** - * A reference to the application's organization model. - */ - private Organization organization; - - public ReadStudentsFile(Organization organization) { - this.organization = organization; - } - @Override public boolean execute(String[] args) { if (args.length < 2) { @@ -37,7 +31,7 @@ public class ReadStudentsFile implements Executable { if (teams == null) { return false; } - this.organization.setStudentTeams(teams); + InitializerApp.organization.setStudentTeams(teams); return true; } } diff --git a/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java index 59155d4..de06018 100644 --- a/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java +++ b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java @@ -1,15 +1,12 @@ package nl.andrewlalis.ui.view; -import com.sun.org.apache.xpath.internal.operations.Or; import nl.andrewlalis.model.Organization; -import nl.andrewlalis.model.StudentTeam; import nl.andrewlalis.ui.control.OutputTextHandler; import nl.andrewlalis.ui.control.command.CommandExecutor; import nl.andrewlalis.ui.control.listeners.*; import javax.swing.*; import java.awt.*; -import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -44,19 +41,16 @@ public class InitializerApp extends JFrame { /** * The organization object, which contains all important state information. */ - private Organization organization; + public static Organization organization; /** * Constructs a new instance of the main application frame, with both an executor, and organization model. * * @param executor The command executor, which is passed to any action listeners, so that buttons in this interface * may execute commands in the same way that the command line can. - * @param organization A reference to the application's organization model, which holds all important runtime state - * information. */ - public InitializerApp(CommandExecutor executor, Organization organization) { + public InitializerApp(CommandExecutor executor) { this.executor = executor; - this.organization = organization; // UI initialization. ImageIcon icon = new ImageIcon(getClass().getResource("/image/icon.png")); diff --git a/src/main/java/nl/andrewlalis/util/TeamGenerator.java b/src/main/java/nl/andrewlalis/util/TeamGenerator.java index b765e7b..96800ed 100644 --- a/src/main/java/nl/andrewlalis/util/TeamGenerator.java +++ b/src/main/java/nl/andrewlalis/util/TeamGenerator.java @@ -2,6 +2,9 @@ package nl.andrewlalis.util; import nl.andrewlalis.model.Student; import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.model.error.Error; +import nl.andrewlalis.model.error.Severity; +import nl.andrewlalis.ui.view.InitializerApp; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; @@ -36,6 +39,7 @@ public class TeamGenerator { logger.fine("Generating teams of size " + teamSize); if (teamSize < 1) { logger.severe("Invalid team size."); + InitializerApp.organization.addError(new Error(Severity.CRITICAL, "Invalid team size while generating teams from CSV.")); throw new IllegalArgumentException("StudentTeam size must be greater than or equal to 1. Got " + teamSize); } logger.fine("Parsing CSV file."); @@ -47,6 +51,7 @@ public class TeamGenerator { studentMap = readAllStudents(records, teamSize); } catch (ArrayIndexOutOfBoundsException e) { logger.severe("StudentTeam size does not match column count in records."); + InitializerApp.organization.addError(new Error(Severity.CRITICAL, "Team size does not match column count in records.")); throw new IllegalArgumentException("StudentTeam size does not match column count in records."); }