diff --git a/pom.xml b/pom.xml
index 1d8e0c8..7137563 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,10 @@
jar
+
+ 2.9.6
+
+
org.apache.commons
@@ -43,6 +47,38 @@
16.0.2
compile
+
+ org.apache.httpcomponents
+ httpclient
+ RELEASE
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+
+ org.kohsuke
+ github-api
+ 1.93
+
+
+
+ org.xerial
+ sqlite-jdbc
+ 3.23.1
+
\ No newline at end of file
diff --git a/src/main/java/nl/andrewlalis/Main.java b/src/main/java/nl/andrewlalis/Main.java
index 3aa4015..b6e7202 100644
--- a/src/main/java/nl/andrewlalis/Main.java
+++ b/src/main/java/nl/andrewlalis/Main.java
@@ -1,16 +1,26 @@
package nl.andrewlalis;
-import nl.andrewlalis.model.Team;
+import nl.andrewlalis.model.database.Database;
+import nl.andrewlalis.git_api.GithubManager;
+import nl.andrewlalis.model.Student;
+import nl.andrewlalis.model.StudentTeam;
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.control.command.Executable;
+import nl.andrewlalis.ui.control.command.executables.ArchiveRepos;
+import nl.andrewlalis.ui.control.command.executables.GenerateAssignmentsRepo;
+import nl.andrewlalis.ui.control.command.executables.ReadStudentsFileToDB;
+import nl.andrewlalis.ui.view.InitializerApp;
+import nl.andrewlalis.util.CommandLine;
import nl.andrewlalis.util.Logging;
import nl.andrewlalis.util.TeamGenerator;
+import javax.swing.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.logging.Level;
import java.util.logging.Logger;
-import nl.andrewlalis.util.CommandLine;
-
/**
* Main program entry point.
*/
@@ -26,26 +36,48 @@ public class Main {
// Initialize logger.
try {
Logging.setup(true); // TODO: Replace true with command line arg.
+
} catch (IOException e) {
logger.severe("Unable to save log to file.");
}
- logger.info("Initializer for Github Repositories in Educational Organizations.");
+ // Command executor which will be used by all actions the user can do.
+ CommandExecutor executor = new CommandExecutor();
+
+ // Initialize User Interface.
+ InitializerApp app = new InitializerApp(executor);
+ app.begin();
+
+ Database db = new Database("database/initializer.sqlite");
+ db.initialize();
+
+ executor.registerCommand("readstudents", new ReadStudentsFileToDB(db));
+ executor.registerCommand("archiveall", new ArchiveRepos());
+ executor.registerCommand("generateassignments", new GenerateAssignmentsRepo());
+
+ logger.info("GithubManager for Github Repositories in Educational Organizations. Program initialized.");
+
+
+
+ // Get studentTeams from CSV file.
+// List studentTeams = getStudentTeamsFromCSV(userOptions.get("input"), Integer.parseInt(userOptions.get("teamsize")));
+//
+// GithubManager githubManager = new GithubManager(
+// userOptions.get("organization"),
+// userOptions.get("token"),
+// "assignments_2018",
+// "teaching-assistants",
+// "advoop_2018"
+// );
- // Get teams from CSV file.
- List teams;
try {
- teams = TeamGenerator.generateFromCSV(
- userOptions.get("input"),
- Integer.parseInt(userOptions.get("teamsize"))
- );
- logger.info("Teams created: " + teams);
- } catch (IOException | ArrayIndexOutOfBoundsException e) {
- logger.severe("Unable to generate teams from CSV file, exiting.");
- System.exit(1);
+ //githubManager.initializeGithubRepos(studentTeams);
+ //githubManager.archiveAllRepositories("team");
+ } catch (Exception e) {
+ e.printStackTrace();
}
-
-
-
}
+
+
+
}
diff --git a/src/main/java/nl/andrewlalis/git_api/GithubManager.java b/src/main/java/nl/andrewlalis/git_api/GithubManager.java
new file mode 100644
index 0000000..5de6ec7
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/git_api/GithubManager.java
@@ -0,0 +1,262 @@
+package nl.andrewlalis.git_api;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import nl.andrewlalis.model.Student;
+import nl.andrewlalis.model.StudentTeam;
+import nl.andrewlalis.model.TATeam;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.kohsuke.github.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * This class is responsible for initializing the Github repositories and setting permissions, adding teams, etc.
+ */
+public class GithubManager {
+
+ /**
+ * The assignments repository where students will get assignments from.
+ */
+ private GHRepository assignmentsRepo;
+
+ /**
+ * Github object for API interactions.
+ */
+ private GitHub github;
+ private GHOrganization organization;
+ private String accessToken;
+
+ /**
+ * The logger for outputting debug info.
+ */
+ private static final Logger logger = Logger.getLogger(GithubManager.class.getName());
+ static {
+ logger.setParent(Logger.getGlobal());
+ }
+
+ public GithubManager(String organizationName, String accessToken) {
+ this.accessToken = accessToken;
+ try {
+ this.github = GitHub.connectUsingOAuth(accessToken);
+ this.organization = this.github.getOrganization(organizationName);
+ } catch (IOException e) {
+ logger.severe("Unable to make a GithubManager with organization name: " + organizationName + " and access token: " + accessToken);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 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.
+ * @param description The description of the repository.
+ * @param allTeachingAssistants The name of the team consisting of all teaching assistants.
+ * @throws IOException If an HTTP request failed.
+ */
+ public void setupAssignmentsRepo(String assignmentsRepoName, String description, String allTeachingAssistants) throws IOException {
+ GHTeam team = this.organization.getTeamByName(allTeachingAssistants);
+ // Check if the repository already exists.
+ GHRepository existingRepo = this.organization.getRepository(assignmentsRepoName);
+ if (existingRepo != null) {
+ existingRepo.delete();
+ logger.fine("Deleted pre-existing assignments repository.");
+ }
+
+ this.assignmentsRepo = this.createRepository(assignmentsRepoName, team, description, false, true, false);
+
+ if (this.assignmentsRepo == null) {
+ logger.severe("Could not create assignments repository.");
+ return;
+ }
+
+ this.protectMasterBranch(this.assignmentsRepo, team);
+
+ // Grant all teaching assistants write access.
+ team.add(this.assignmentsRepo, GHOrganization.Permission.ADMIN);
+ logger.fine("Gave admin rights to all teaching assistants in team: " + team.getName());
+ }
+
+ /**
+ * Creates and sets up a student team's repository, and invites those students to the organization's assignments
+ * repository as well.
+ * @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 {
+ // 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);
+
+ if (repo == null) {
+ logger.severe("Repository for student team " + team.getId() + " could not be created.");
+ return;
+ }
+
+ this.protectMasterBranch(repo, taTeam);
+ this.createDevelopmentBranch(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);
+ }
+
+ /**
+ * Deletes all repositories in the organization.
+ * @throws IOException if an error occurs with sending requests.
+ */
+ public void deleteAllRepositories() throws IOException {
+ List repositories = this.organization.listRepositories().asList();
+ for (GHRepository repo : repositories) {
+ repo.delete();
+ }
+ }
+
+ /**
+ * 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 {
+ List repositories = this.organization.listRepositories().asList();
+ for (GHRepository repo : repositories) {
+ if (repo.getName().contains(sub)) {
+ archiveRepository(repo);
+ }
+ }
+ }
+
+ /**
+ * 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());
+ }
+ logger.info("Archived repository: " + repo.getFullName());
+ }
+
+ /**
+ * Protects the master branch of a given repository, and gives admin rights to the given team.
+ * @param repo The repository to protect the master branch of.
+ * @param team The team which gets admin rights to the master branch.
+ */
+ @SuppressWarnings("deprecation")
+ private void protectMasterBranch(GHRepository repo, GHTeam team) {
+ try {
+ GHBranchProtectionBuilder protectionBuilder = repo.getBranch("master").enableProtection();
+ protectionBuilder.includeAdmins(false);
+ protectionBuilder.restrictPushAccess();
+ protectionBuilder.teamPushAccess(team);
+ protectionBuilder.addRequiredChecks("ci/circleci");
+ protectionBuilder.enable();
+ logger.fine("Protected master branch of repository: " + repo.getName());
+ } catch (IOException e) {
+ logger.severe("Could not protect master branch of repository: " + repo.getName());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Creates a development branch for the given repository.
+ * @param repo The repository to create a development branch for.
+ */
+ private void createDevelopmentBranch(GHRepository repo) {
+ try {
+ String sha1 = repo.getBranch(repo.getDefaultBranch()).getSHA1();
+ repo.createRef("refs/heads/development", sha1);
+ logger.fine("Created development branch of repository: " + repo.getName());
+ } catch (IOException e) {
+ logger.severe("Could not create development branch for repository: " + repo.getName() + '\n' + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Creates a new github repository.
+ * @param name The name of the repository.
+ * @param taTeam The team to give admin rights.
+ * @param description The description of the repository.
+ * @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
+ */
+ private GHRepository createRepository(String name, GHTeam taTeam, String description, boolean hasWiki, boolean hasIssues, boolean isPrivate){
+ try {
+ GHCreateRepositoryBuilder builder = this.organization.createRepository(name);
+ builder.team(taTeam);
+ builder.wiki(hasWiki);
+ builder.issues(hasIssues);
+ builder.description(description);
+ builder.gitignoreTemplate("Java");
+ builder.private_(isPrivate); // TODO: Change this to true for production
+ 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());
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/git_api/package-info.java b/src/main/java/nl/andrewlalis/git_api/package-info.java
new file mode 100644
index 0000000..097515a
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/git_api/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Contains all logic which directly interacts with the Github API. All methods which need to perform an action on a
+ * repository or team should find it within this package.
+ *
+ * @author Andrew Lalis
+ */
+package nl.andrewlalis.git_api;
\ No newline at end of file
diff --git a/src/main/java/nl/andrewlalis/model/Person.java b/src/main/java/nl/andrewlalis/model/Person.java
new file mode 100644
index 0000000..66b0fb7
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/Person.java
@@ -0,0 +1,89 @@
+package nl.andrewlalis.model;
+
+/**
+ * A generic object that students, teaching assistants, and professors can extend from. This covers all the basic
+ * functionality that applies to anyone in the system.
+ */
+public abstract class Person {
+
+ /**
+ * The unique identification number for this person. (P- or S-Number)
+ */
+ protected int number;
+
+ /**
+ * The person's first and last name.
+ */
+ protected String name;
+
+ /**
+ * The person's email address.
+ */
+ protected String emailAddress;
+
+ /**
+ * The person's github username.
+ */
+ protected String githubUsername;
+
+ /**
+ * Constructs a Person from all the basic information needed.
+ * @param number Either an S- or P-Number without the letter prefix.
+ * @param name The first, middle (if applicable) and last name.
+ * @param emailAddress The email address. (Either university or personal.)
+ * @param githubUsername The person's github username.
+ */
+ public Person(int number, String name, String emailAddress, String githubUsername){
+ this.number = number;
+ this.name = name;
+ this.emailAddress = emailAddress;
+ this.githubUsername = githubUsername;
+ }
+
+ /**
+ * Accessors
+ */
+ public int getNumber(){
+ return this.number;
+ }
+
+ public String getName(){
+ return this.name;
+ }
+
+ public String getEmailAddress(){
+ return this.emailAddress;
+ }
+
+ public String getGithubUsername(){
+ return this.githubUsername;
+ }
+
+ /**
+ * Determines if two persons are the same. This is defined as:
+ * Two persons are equal if at least one of their personal data points is identical. Because each of the data points
+ * should be unique to this person alone, if there is any conflict, assume that they are equal.
+ * @param o The object to compare to.
+ * @return True if the two persons share personal data, or false if all data is unique among the two.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Person)) {
+ return false;
+ }
+ Person p = (Person)o;
+ return p.getNumber() == this.getNumber()
+ || p.getEmailAddress().equals(this.getEmailAddress())
+ || p.getGithubUsername().equals(this.getGithubUsername())
+ || p.getName().equalsIgnoreCase(this.getName());
+ }
+
+ /**
+ * Represents the person as a basic comma-separated string object.
+ * @return A comma-separated String object.
+ */
+ @Override
+ public String toString() {
+ return this.getName() + ", " + this.getNumber() + ", " + this.getEmailAddress() + ", " + this.getGithubUsername();
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/model/Student.java b/src/main/java/nl/andrewlalis/model/Student.java
index d268ab5..ec8712b 100644
--- a/src/main/java/nl/andrewlalis/model/Student.java
+++ b/src/main/java/nl/andrewlalis/model/Student.java
@@ -1,66 +1,34 @@
package nl.andrewlalis.model;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Represents one student's github information.
*/
-public class Student {
-
- /**
- * The student's S-number.
- */
- private int number;
-
- /**
- * The student's name.
- */
- private String name;
-
- /**
- * The student's email.
- */
- private String emailAddress;
-
- /**
- * The student's github username.
- */
- private String githubUsername;
+public class Student extends Person {
/**
* A list of partners that the student has said that they would like to be partners with.
*/
private List preferredPartners;
+ /**
+ * Constructs a student similarly to a Person, but with an extra preferredPartners list.
+ * @param number The student's S-Number.
+ * @param name The student's name.
+ * @param emailAddress The student's email address.
+ * @param githubUsername The student's github username.
+ * @param preferredPartners A list of this student's preferred partners, as a list of integers representing the
+ * other students' numbers.
+ */
public Student(int number, String name, String emailAddress, String githubUsername, List preferredPartners) {
- this.number = number;
- this.name = name;
- this.emailAddress = emailAddress;
- this.githubUsername = githubUsername;
+ super(number, name, emailAddress, githubUsername);
this.preferredPartners = preferredPartners;
}
- @Override
- public String toString() {
- return this.number + " - " + this.name + " - " + this.emailAddress + " - " + this.githubUsername;
- }
-
- public int getNumber() {
- return number;
- }
-
- public String getEmailAddress() {
- return emailAddress;
- }
-
- public String getGithubUsername() {
- return githubUsername;
- }
-
public List getPreferredPartners() {
- return preferredPartners;
+ return this.preferredPartners;
}
/**
@@ -68,23 +36,12 @@ public class Student {
* @param studentMap A mapping from student number to student for all students who have signed up.
* @return A team with unknown id, comprised of this student's preferred partners.
*/
- public Team getPreferredTeam(Map studentMap) {
- Team t = new Team();
+ public StudentTeam getPreferredTeam(Map studentMap) {
+ StudentTeam t = new StudentTeam();
for (int partnerNumber : this.getPreferredPartners()) {
- t.addStudent(studentMap.get(partnerNumber));
+ t.addMember(studentMap.get(partnerNumber));
}
- t.addStudent(this);
+ t.addMember(this);
return t;
}
-
- @Override
- public boolean equals(Object s) {
- if (!(s instanceof Student)) {
- return false;
- }
- Student student = (Student) s;
- return student.getNumber() == this.getNumber()
- || student.getEmailAddress().equals(this.getEmailAddress())
- || student.getGithubUsername().equals(this.getGithubUsername());
- }
}
diff --git a/src/main/java/nl/andrewlalis/model/StudentTeam.java b/src/main/java/nl/andrewlalis/model/StudentTeam.java
new file mode 100644
index 0000000..8967d27
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/StudentTeam.java
@@ -0,0 +1,76 @@
+package nl.andrewlalis.model;
+
+import java.util.Arrays;
+
+/**
+ * Represents one or more students' collective information.
+ */
+public class StudentTeam extends Team{
+
+ public StudentTeam() {
+ super(-1);
+ }
+
+ /**
+ * Gets a list of students, casted from the original Person[].
+ * @return An array of Students.
+ */
+ public Student[] getStudents() {
+ return Arrays.copyOf(this.getMembers(), this.memberCount(), Student[].class);
+ }
+
+ /**
+ * Determines if a team is valid, and ready to be added to the Github organization.
+ * A team is valid if and only if:
+ * - The student count is equal to the team size.
+ * - Each student is unique.
+ * - Each student's preferred partners match all the others.
+ * @param teamSize The preferred size of teams.
+ * @return True if the team is valid, and false otherwise.
+ */
+ public boolean isValid(int teamSize) {
+ if (this.memberCount() == teamSize) {
+ for (Student studentA : this.getStudents()) {
+ for (Student studentB : this.getStudents()) {
+ if (!studentA.equals(studentB) && !studentA.getPreferredPartners().contains(studentB.getNumber())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Generates a unique name which is intended to be used for the repository name of this team.
+ * @param prefix A prefix to further reduce the chances of duplicate names.
+ * It is suggested to use something like "2018_OOP"
+ * @return A string comprised of the prefix, team id, and student number of each team member.
+ */
+ public String generateUniqueName(String prefix) {
+ StringBuilder sb = new StringBuilder(prefix);
+ sb.append("_team_").append(this.id);
+ for (Student s : this.getStudents()) {
+ sb.append('_').append(s.getNumber());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a description for the repository, based on the students' names and group number.
+ * @return A description for the students' repository.
+ */
+ public String generateRepoDescription() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Group ").append(this.id).append(": ");
+ for (int i = 0; i < this.memberCount(); i++) {
+ sb.append(this.getStudents()[i].getName());
+ if (i != this.memberCount()-1) {
+ sb.append(", ");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/model/TATeam.java b/src/main/java/nl/andrewlalis/model/TATeam.java
new file mode 100644
index 0000000..2e252df
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/TATeam.java
@@ -0,0 +1,72 @@
+package nl.andrewlalis.model;
+
+import org.kohsuke.github.GHOrganization;
+import org.kohsuke.github.GHTeam;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a teaching assistant team, which is itself a 'team' in the organization. This class is used for parsing
+ * json from requests to github to get a list of all teams in the organization.
+ */
+public class TATeam {
+
+ private List teachingAssistants;
+
+ /**
+ * The team's display name.
+ */
+ private String name;
+
+ /**
+ * The team's unique identifier.
+ */
+ private int id;
+
+ /**
+ * The Github team associated with this team.
+ */
+ private GHTeam githubTeam;
+
+ /**
+ * Constructs a team without any teaching assistant members.
+ * @param name The name of the team.
+ * @param id The unique identifier for this team.
+ */
+ public TATeam(String name, int id) {
+ this.name = name;
+ this.id = id;
+ this.teachingAssistants = new ArrayList();
+ }
+
+ /**
+ * Constructs a team with a list of teaching assistants that are part of it.
+ * @param teachingAssistants The list of teaching assistants that are part of the team.
+ */
+ public TATeam(List teachingAssistants, String name, int id) {
+ this.teachingAssistants = teachingAssistants;
+ this.name = name;
+ this.id = id;
+ }
+
+ /**
+ * Gets the unique identification for this TA team.
+ * @return An integer representing the id of this team.
+ */
+ public int getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public GHTeam getGithubTeam() {
+ return this.githubTeam;
+ }
+
+ public void setGithubTeam(GHTeam team) {
+ this.githubTeam = team;
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/model/TeachingAssistant.java b/src/main/java/nl/andrewlalis/model/TeachingAssistant.java
new file mode 100644
index 0000000..918bb5c
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/TeachingAssistant.java
@@ -0,0 +1,16 @@
+package nl.andrewlalis.model;
+
+public class TeachingAssistant extends Person {
+
+ /**
+ * Constructs a Teaching Assistant from all the basic information needed, much like its parent, Person.
+ *
+ * @param number Either an S- or P-Number without the letter prefix.
+ * @param name The first, middle (if applicable) and last name.
+ * @param emailAddress The email address. (Either university or personal.)
+ * @param githubUsername The person's github username.
+ */
+ public TeachingAssistant(int number, String name, String emailAddress, String githubUsername) {
+ super(number, name, emailAddress, githubUsername);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/model/Team.java b/src/main/java/nl/andrewlalis/model/Team.java
index bd82bd2..f1b23db 100644
--- a/src/main/java/nl/andrewlalis/model/Team.java
+++ b/src/main/java/nl/andrewlalis/model/Team.java
@@ -1,147 +1,153 @@
package nl.andrewlalis.model;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
- * Represents one or more students' collective information.
+ * An abstract Team object from which both Teaching Assistant and Student teams can be built. A Team consists of a list
+ * of members, and a unique identification number.
*/
-public class Team {
+public abstract class Team {
/**
- * The list of students in this team.
+ * An identification number unique to this team alone.
*/
- private List students;
+ protected int id;
/**
- * The team identification number.
+ * A list of members of this team.
*/
- private int id;
+ private List members;
- public Team() {
- this.students = new ArrayList<>();
- this.id = -1;
+ /**
+ * Constructs this team with the given id.
+ * @param id The id to assign to this team.
+ */
+ public Team(int id) {
+ this.id = id;
+ this.members = new ArrayList<>();
}
/**
- * Determines if a student is already included in this team.
- * @param student A student.
- * @return True if the student is in this team, false otherwise.
+ * @param newId The new id number to assign to this team.
*/
- public boolean hasStudent(Student student) {
- for (Student s : this.students) {
- if (s.equals(student)) {
+ public void setId(int newId) {
+ this.id = newId;
+ }
+
+ /**
+ * @return This team's id number.
+ */
+ public int getId() {
+ return this.id;
+ }
+
+ /**
+ * Adds a new person to this team, only if they do not exist in this team yet.
+ * @param newMember The new member to add.
+ */
+ public void addMember(Person newMember) {
+ for (Person person : this.members) {
+ if (person.equals(newMember)) {
+ return;
+ }
+ }
+ this.members.add(newMember);
+ }
+
+ /**
+ * Removes a person from this team.
+ * @param person The person to remove.
+ */
+ public void removeMember(Person person) {
+ this.members.remove(person);
+ }
+
+ /**
+ * Checks if this team contains the given person.
+ * @param person The person to check for.
+ * @return True if the person is a member of this team, false otherwise.
+ */
+ public boolean containsMember(Person person) {
+ for (Person p : this.members) {
+ if (p.equals(person)) {
return true;
}
}
return false;
}
- public int getStudentCount() {
- return this.students.size();
- }
-
- public int getId() {
- return this.id;
- }
-
- public void setId(int id) {
- this.id = id;
- }
-
- public void setStudents(List students) {
- this.students = students;
- }
-
- public List getStudents() {
- return this.students;
+ /**
+ * Sets the team to be comprised of only the members given in the array.
+ * @param people The people which will make up the members of this team.
+ */
+ public void setMembers(Person[] people) {
+ this.members = new ArrayList<>(Arrays.asList(people));
}
/**
- * Adds a student to this team.
- * @param student The student to add.
- * @return True if the student could be added, false otherwise.
+ * Gets a list of people in this team.
+ * @return A list of people in this team.
*/
- public boolean addStudent(Student student) {
- if (!this.hasStudent(student)) {
- this.students.add(student);
- return true;
- } else {
- return false;
- }
+ public Person[] getMembers() {
+ Person[] people = new Person[this.memberCount()];
+ this.members.toArray(people);
+ return people;
}
/**
- * Determines if a team is valid, and ready to be added to the Github organization.
- * A team is valid if and only if:
- * - The student count is equal to the team size.
- * - Each student is unique.
- * - Each student's preferred partners match all the others.
- * @param teamSize The preferred size of teams.
- * @return True if the team is valid, and false otherwise.
+ * Gets the number of people in this team.
+ * @return The number of people in this team.
*/
- public boolean isValid(int teamSize) {
- if (this.getStudentCount() == teamSize) {
- List encounteredIds = new ArrayList<>();
- for (Student studentA : this.students) {
- for (Student studentB : this.students) {
- if (!studentA.equals(studentB) && !studentA.getPreferredPartners().contains(studentB.getNumber())) {
- return false;
- }
- }
- }
- return true;
- } else {
- return false;
- }
+ public int memberCount() {
+ return this.members.size();
}
/**
- * Generates a unique name which is intended to be used for the repository name of this team.
- * @param prefix A prefix to further reduce the chances of duplicate names.
- * It is suggested to use something like "2018_OOP"
- * @return A string comprised of the prefix, team id, and student number of each team member.
+ * Determines if another team has the same members as this team.
+ * @param team The team to compare to this team.
+ * @return True if the other team has all the same members as this team.
*/
- public String generateUniqueName(String prefix) {
- StringBuilder sb = new StringBuilder(prefix);
- sb.append("_team_").append(this.id);
- for (Student s : this.students) {
- sb.append('_').append(s.getNumber());
- }
- return sb.toString();
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder("Team: ");
- sb.append(this.id).append('\n');
- for (Student s : this.students) {
- sb.append('\t').append(s.toString()).append('\n');
- }
- return sb.toString();
- }
-
- /**
- * Determines if one team is equivalent to another. This is determined by if the two teams are comprised of the same
- * students, in any order.
- * @param o The object to compare to this team.
- * @return True if the teams contain the same students, false otherwise.
- */
- @Override
- public boolean equals(Object o) {
- if (o instanceof Team) {
- Team t = (Team) o;
- if (t.getStudentCount() != this.getStudentCount()) {
- return false;
- }
- for (Student s : this.students) {
- if (!t.hasStudent(s)) {
+ public boolean hasSameMembers(Team team) {
+ if (this.memberCount() == team.memberCount()) {
+ for (Person person : this.members) {
+ if (!team.containsMember(person)) {
return false;
}
}
return true;
- } else {
- return false;
}
+ return false;
}
+
+ /**
+ * Checks if an object is equal to this team. First checks if the other object is a Team, and then if it has the
+ * same id and team size. If both of those conditions are met, then it will check that all team members are the
+ * same.
+ * @param obj The object to check for equality.
+ * @return True if the two objects represent the same team, or false otherwise.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Team) {
+ Team team = (Team) obj;
+ return team.getId() == this.getId() && this.hasSameMembers(team);
+ }
+ return false;
+ }
+
+ /**
+ * @return A String containing a line for each member in the team.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Team of ").append(this.memberCount()).append(" members:\tID: ").append(this.id).append('\n');
+ for (Person person : this.members) {
+ sb.append(person.toString()).append('\n');
+ }
+ return sb.toString();
+ }
+
}
diff --git a/src/main/java/nl/andrewlalis/model/database/Database.java b/src/main/java/nl/andrewlalis/model/database/Database.java
new file mode 100644
index 0000000..14c8626
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/database/Database.java
@@ -0,0 +1,261 @@
+package nl.andrewlalis.model.database;
+
+import nl.andrewlalis.model.*;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * This class abstracts many of the functions needed for interaction with the application's SQLite database.
+ */
+public class Database {
+
+ private static final int PERSON_TYPE_STUDENT = 0;
+ private static final int PERSON_TYPE_TA = 1;
+
+ private static final int TEAM_TYPE_STUDENT = 0;
+ private static final int TEAM_TYPE_TA = 1;
+ private static final int TEAM_TYPE_TA_ALL = 2;
+
+ private static final int TEAM_NONE = 1000000;
+ private static final int TEAM_TA_ALL = 1000001;
+
+ private static final int ERROR_TYPE_TEAM = 0;
+ private static final int ERROR_TYPE_PERSON = 1;
+ private static final int ERROR_TYPE_SYSTEM = 2;
+
+ /**
+ * The connection needed for all queries.
+ */
+ private Connection connection;
+
+ /**
+ * The logger for outputting debug info.
+ */
+ private static final Logger logger = Logger.getLogger(Database.class.getName());
+ static {
+ logger.setParent(Logger.getGlobal());
+ }
+
+ public Database(String databaseFilename) {
+ try {
+ this.connection = DriverManager.getConnection("jdbc:sqlite:" + databaseFilename);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Initializes the database from the table_init.sql script, which defines the table schema.
+ * Then, inserts some constant starter data from /sql/insert/types.sql.
+ * @return True if successful, false if not.
+ */
+ public boolean initialize() {
+ List tableStatements = Utils.prepareStatementsFromFile("/sql/table_init.sql", this.connection);
+ for (PreparedStatement statement : tableStatements) {
+ try {
+ statement.execute();
+ } catch (SQLException e) {
+ logger.severe("SQLException while executing prepared statement:\n" + statement.toString() + "\nCode: " + e.getErrorCode());
+ return false;
+ }
+ }
+ logger.fine("Database tables initialized.");
+ List insertStatements = Utils.prepareStatementsFromFile("/sql/insert/types.sql", this.connection);
+ for (PreparedStatement statement : insertStatements) {
+ try {
+ statement.execute();
+ } catch (SQLException e) {
+ logger.severe("SQLException while inserting into table:\n" + statement.toString() + "\nCode: " + e.getErrorCode());
+ return false;
+ }
+ }
+ logger.fine("Initial types inserted.");
+ return true;
+ }
+
+ /**
+ * Stores a person in the database.
+ * @param person The person object to store.
+ * @param personType The type of person to store, using a constant defined above.
+ * @return True if successful, false otherwise.
+ */
+ private boolean storePerson(Person person, int personType) {
+ try {
+ logger.finest("Storing person: " + person);
+ String sql = "INSERT INTO persons (id, name, email_address, github_username, person_type_id) VALUES (?, ?, ?, ?, ?);";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, person.getNumber());
+ stmt.setString(2, person.getName());
+ stmt.setString(3, person.getEmailAddress());
+ stmt.setString(4, person.getGithubUsername());
+ stmt.setInt(5, personType);
+ stmt.execute();
+ return true;
+ } catch (SQLException e) {
+ logger.severe("SQLException while inserting Person: " + person + '\n' + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Retrieves a list of preferred partners that each student has set.
+ * @param studentId The student id to search by.
+ * @return A list of student id's for all students that the given student wishes to be their partner.
+ */
+ private List retrievePreferredPartners(int studentId) {
+ try {
+ logger.finest("Retrieving preferred partners of student: " + studentId);
+ String sql = "SELECT partner_id FROM student_preferred_partners WHERE student_id=?;";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, studentId);
+ ResultSet results = stmt.executeQuery();
+ List partners = new ArrayList<>();
+ while (results.next()) {
+ partners.add(results.getInt(1));
+ }
+ return partners;
+ } catch (SQLException e) {
+ logger.severe("SQL Exception while retrieving preferred partners of student: " + studentId + '\n' + e.getMessage());
+ e.printStackTrace();
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Retrieves a student by their id.
+ * @param id The id of the student (student number)
+ * @return The student corresponding to this number, or null if it could not be found.
+ */
+ public Student retrieveStudent(int id) {
+ try {
+ String sql = "SELECT * FROM persons WHERE id=?";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, id);
+ ResultSet result = stmt.executeQuery();
+ return new Student(id, result.getString("name"), result.getString("email_address"), result.getString("github_username"), this.retrievePreferredPartners(id));
+ } catch (SQLException e) {
+ logger.severe("SQL Exception while retrieving Student.\n" + e.getMessage());
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Stores a teaching assistant without a team.
+ * @param ta The teaching assistant to store.
+ * @return True if successful, false otherwise.
+ */
+ public boolean storeTeachingAssistant(TeachingAssistant ta) {
+ return this.storeTeachingAssistant(ta, TEAM_NONE);
+ }
+
+ /**
+ * Stores a teaching assistant in the database.
+ * @param ta The teaching assistant to store.
+ * @param teamId The teaching assistant's team id.
+ * @return True if successful, false otherwise.
+ */
+ public boolean storeTeachingAssistant(TeachingAssistant ta, int teamId) {
+ if (!storePerson(ta, PERSON_TYPE_TA)) {
+ return false;
+ }
+ try {
+ String sql = "INSERT INTO teaching_assistants (person_id, team_id) VALUES (?, ?);";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, ta.getNumber());
+ stmt.setInt(2, teamId);
+ stmt.execute();
+ return true;
+ } catch (SQLException e) {
+ logger.severe("SQL Exception while inserting TeachingAssistant.\n" + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Stores a team in the database.
+ * @param team The team to store.
+ * @param type The type of team that this is.
+ * @return True if successful, false otherwise.
+ */
+ public boolean storeTeam(Team team, int type) {
+ try {
+ String sql = "INSERT INTO teams (id, team_type_id) VALUES (?, ?);";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, team.getId());
+ stmt.setInt(2, type);
+ stmt.execute();
+ return true;
+ } catch (SQLException e) {
+ logger.severe("SQLException while inserting team: " + team + '\n' + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Stores a list of student teams in the database.
+ * @param teams The list of teams to store.
+ * @return True if successful, or false if an error occurred.
+ */
+ public boolean storeStudentTeams(List teams) {
+ for (StudentTeam team : teams) {
+ if (!this.storeTeam(team, TEAM_TYPE_STUDENT)) {
+ return false;
+ }
+ for (Student student : team.getStudents()) {
+ if (!this.storeStudent(student, team.getId())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Stores a student without a team.
+ * @param student The student to store.
+ * @return True if successful, false otherwise.
+ */
+ public boolean storeStudent(Student student) {
+ return this.storeStudent(student, TEAM_NONE);
+ }
+
+ /**
+ * Stores a student in the database.
+ * @param student The student to store.
+ * @param teamId The team id for the team the student is in.
+ * @return True if the operation was successful, false otherwise.
+ */
+ public boolean storeStudent(Student student, int teamId) {
+ logger.finest("Storing student: " + student);
+ if (!storePerson(student, PERSON_TYPE_STUDENT)) {
+ return false;
+ }
+ try {
+ String sql = "INSERT INTO students (person_id, team_id, chose_partner) VALUES (?, ?, ?);";
+ PreparedStatement stmt = this.connection.prepareStatement(sql);
+ stmt.setInt(1, student.getNumber());
+ stmt.setInt(2, teamId);
+ stmt.setInt(3, student.getPreferredPartners().size() > 0 ? 1 : 0);
+ stmt.execute();
+ // Storing partners.
+ String sqlPartner = "INSERT INTO student_preferred_partners (student_id, partner_id) VALUES (?, ?);";
+ PreparedStatement stmtPartner = this.connection.prepareStatement(sqlPartner);
+ for (int partnerId : student.getPreferredPartners()) {
+ stmtPartner.setInt(1, student.getNumber());
+ stmtPartner.setInt(2, partnerId);
+ stmtPartner.execute();
+ }
+ return true;
+ } catch (SQLException e) {
+ logger.severe("SQL Exception while inserting Student into database.\n" + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/model/database/Utils.java b/src/main/java/nl/andrewlalis/model/database/Utils.java
new file mode 100644
index 0000000..e908f1d
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/database/Utils.java
@@ -0,0 +1,52 @@
+package nl.andrewlalis.model.database;
+
+import nl.andrewlalis.util.FileUtils;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Contains some methods which make database actions much easier.
+ */
+public class Utils {
+
+ /**
+ * The logger for outputting debug info.
+ */
+ private static final Logger logger = Logger.getLogger(Utils.class.getName());
+ static {
+ logger.setParent(Logger.getGlobal());
+ }
+
+ /**
+ * Gets an ordered list of prepared statements from a file which contains multiple statements separated by a
+ * semicolon. This method separates those statements into their own strings, and prepares them individually to be
+ * executed later.
+ * @param filename The name of the file which contains the statements.
+ * @param connection The connection to a database; used to prepare statements.
+ * @return An ordered list of prepared statements which are based on the contents of the file provided.
+ */
+ public static List prepareStatementsFromFile(String filename, Connection connection) {
+ String string = FileUtils.readStringFromFile(filename);
+ if (string == null || string.isEmpty()) {
+ return new ArrayList<>();
+ }
+ String[] splits = string.split(";");
+ List statements = new ArrayList<>();
+ for (String split : splits) {
+ if (split.trim().length() > 1) {
+ try {
+ statements.add(connection.prepareStatement(split));
+ } catch (SQLException e) {
+ logger.severe("SQLException while preparing a statement:\n" + split + "\nError Code: " + e.getErrorCode() + '\n' + e.getMessage());
+ }
+ }
+ }
+ return statements;
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/model/package-info.java b/src/main/java/nl/andrewlalis/model/package-info.java
new file mode 100644
index 0000000..d8465c9
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/model/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Contains all objects which form the conceptual basis for this application, such as Students, Teams, and any other
+ * abstract data containers.
+ *
+ * @author Andrew Lalis
+ */
+package nl.andrewlalis.model;
\ No newline at end of file
diff --git a/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java b/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java
new file mode 100644
index 0000000..875c148
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/OutputTextHandler.java
@@ -0,0 +1,45 @@
+package nl.andrewlalis.ui.control;
+
+import nl.andrewlalis.ui.view.OutputTextPane;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+
+/**
+ * A custom handler for printing log messages to the user interface text output pane.
+ */
+public class OutputTextHandler extends Handler {
+
+ /**
+ * The pane to which this handler writes.
+ */
+ private OutputTextPane outputPane;
+
+ public OutputTextHandler(OutputTextPane outputPane) {
+ this.outputPane = outputPane;
+ }
+
+ @Override
+ public void publish(LogRecord logRecord) {
+ DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
+ String dateString = df.format(new Date(logRecord.getMillis()));
+ String sourceLocationString = logRecord.getSourceClassName() + "::" + logRecord.getSourceMethodName();
+ this.outputPane.printStyled(dateString + ' ', "gray_italics");
+ this.outputPane.printStyled(logRecord.getLevel().getName() + ": ", "bold");
+ this.outputPane.printStyled(sourceLocationString + "\n\t", "bold");
+ this.outputPane.printStyled(logRecord.getMessage() + '\n', "smaller");
+ }
+
+ @Override
+ public void flush() {
+
+ }
+
+ @Override
+ public void close() throws SecurityException {
+
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java b/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java
new file mode 100644
index 0000000..dd5d84a
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/CommandExecutor.java
@@ -0,0 +1,73 @@
+package nl.andrewlalis.ui.control.command;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * Manages parsing an entered string and executing a task based upon information in the command.
+ */
+public class CommandExecutor {
+
+ /**
+ * The logger for outputting debug info.
+ */
+ private static final Logger logger = Logger.getLogger(CommandExecutor.class.getName());
+ static {
+ logger.setParent(Logger.getGlobal());
+ }
+
+ /**
+ * A list of named commands which can be executed.
+ */
+ private Map commands;
+
+ public CommandExecutor() {
+ this.commands = new HashMap<>();
+ }
+
+ /**
+ * Adds a new command to the list of commands which this executor can handle.
+ * @param commandName The name that the command will be found by.
+ * @param executable The executable command that is bound to the given name.
+ */
+ public void registerCommand(String commandName, Executable executable) {
+ this.commands.put(commandName, executable);
+ logger.fine("Registered command: " + commandName);
+ }
+
+ /**
+ * Attempts to execute a command string, or show an error message if an invalid command or argument was entered.
+ * @param commandString The String command and any arguments that go with it.
+ */
+ public void executeString(String commandString) {
+ String[] words = commandString.trim().split(" ");
+ if (words.length < 1) {
+ logger.warning("No command supplied.");
+ return;
+ }
+ String commandName = words[0];
+ String[] args = new String[words.length - 1];
+ if (words.length > 1) {
+ System.arraycopy(words, 1, args, 0, words.length - 1);
+ }
+ this.executeCommand(commandName, args);
+ }
+
+ /**
+ * Executes a command with the given name, and given arguments.
+ * @param commandName The name of the command. A command must be registered using registerCommand before it can be
+ * called here.
+ * @param args The list of arguments to provide to the command as needed by the executable that was registered.
+ */
+ public void executeCommand(String commandName, String[] args) {
+ if (this.commands.containsKey(commandName)) {
+ logger.info(commandName + ' ' + Arrays.toString(args));
+ this.commands.get(commandName).execute(args);
+ } else {
+ logger.warning(commandName + " is not a valid command.");
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/Executable.java b/src/main/java/nl/andrewlalis/ui/control/command/Executable.java
new file mode 100644
index 0000000..77ed4bd
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/Executable.java
@@ -0,0 +1,17 @@
+package nl.andrewlalis.ui.control.command;
+
+/**
+ * Classes which implement this interface tell that they may be 'executed', either via command-line, or through the use
+ * of user interface actions.
+ */
+public interface Executable {
+
+ /**
+ * Runs this Executable's main functionality.
+ *
+ * @param args The list of arguments supplied to the executable.
+ * @return True if successful, false if an error occurred.
+ */
+ boolean execute(String[] args);
+
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java
new file mode 100644
index 0000000..5e43269
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/ArchiveRepos.java
@@ -0,0 +1,28 @@
+package nl.andrewlalis.ui.control.command.executables;
+
+import nl.andrewlalis.git_api.GithubManager;
+
+import java.io.IOException;
+
+/**
+ * Represents the action archive all repositories with a certain substring in their name.
+ * It takes the following arguments:
+ *
+ * 1. Repo substring to archive by
+ */
+public class ArchiveRepos extends GithubExecutable {
+
+ @Override
+ protected boolean executeWithManager(GithubManager manager, String[] args) {
+ if (args.length < 1) {
+ return false;
+ }
+ try {
+ manager.archiveAllRepositories(args[0]);
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java
new file mode 100644
index 0000000..2ccb06b
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateAssignmentsRepo.java
@@ -0,0 +1,30 @@
+package nl.andrewlalis.ui.control.command.executables;
+
+import nl.andrewlalis.git_api.GithubManager;
+
+import java.io.IOException;
+
+/**
+ * Generates the assignments repository, with the supplied github manager, as well as the following extra arguments:
+ *
+ * 1. The name of the repository.
+ * 2. Description of the repository.
+ * 3. Name of TA team containing all members.
+ */
+public class GenerateAssignmentsRepo extends GithubExecutable {
+
+ @Override
+ protected boolean executeWithManager(GithubManager manager, String[] args) {
+ if (args.length < 3) {
+ return false;
+ }
+ try {
+ manager.setupAssignmentsRepo(args[0], args[1], args[2]);
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..41f7975
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GenerateStudentRepos.java
@@ -0,0 +1,12 @@
+package nl.andrewlalis.ui.control.command.executables;
+
+import nl.andrewlalis.git_api.GithubManager;
+
+public class GenerateStudentRepos extends GithubExecutable {
+
+ @Override
+ protected boolean executeWithManager(GithubManager manager, String[] args) {
+ return false;
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java
new file mode 100644
index 0000000..e7362bd
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/GithubExecutable.java
@@ -0,0 +1,34 @@
+package nl.andrewlalis.ui.control.command.executables;
+
+import nl.andrewlalis.git_api.GithubManager;
+import nl.andrewlalis.ui.control.command.Executable;
+
+/**
+ * Represents an executable which interacts with github, and therefore needs access to a Github
+ * manager to execute.
+ *
+ * Requires two arguments:
+ * 1. The organization name.
+ * 2. The organization's access token.
+ */
+public abstract class GithubExecutable implements Executable {
+
+ @Override
+ public boolean execute(String[] args) {
+ if (args.length < 2) {
+ return false;
+ }
+ String[] extraArgs = new String[args.length-2];
+ System.arraycopy(args, 2, extraArgs, 0, args.length-2);
+ GithubManager manager = new GithubManager(args[0], args[1]);
+ return this.executeWithManager(manager, extraArgs);
+ }
+
+ /**
+ * Executes a command and provides a github manager with which to perform operations.
+ * @param manager The GithubManager used to perform actions on the repositories.
+ * @param args Any additional arguments provided to the executable.
+ * @return True if successful, or false otherwise.
+ */
+ protected abstract boolean executeWithManager(GithubManager manager, String[] args);
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java
new file mode 100644
index 0000000..2d3ddc3
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/command/executables/ReadStudentsFileToDB.java
@@ -0,0 +1,40 @@
+package nl.andrewlalis.ui.control.command.executables;
+
+import nl.andrewlalis.model.StudentTeam;
+import nl.andrewlalis.model.database.Database;
+import nl.andrewlalis.ui.control.command.Executable;
+import nl.andrewlalis.util.FileUtils;
+
+import java.util.List;
+
+/**
+ * Execute this class to read students from a supplied filename and teamsize, and store their
+ * information in the database.
+ * Requires the following arguments:
+ *
+ * 1. filename
+ * 2. teamsize
+ */
+public class ReadStudentsFileToDB implements Executable {
+
+ /**
+ * The database used to store the students.
+ */
+ private Database db;
+
+ public ReadStudentsFileToDB(Database db) {
+ this.db = db;
+ }
+
+
+ @Override
+ public boolean execute(String[] args) {
+ if (args.length < 2) {
+ return false;
+ }
+ String filename = args[0];
+ int teamSize = Integer.parseUnsignedInt(args[1]);
+ List teams = FileUtils.getStudentTeamsFromCSV(filename, teamSize);
+ return this.db.storeStudentTeams(teams);
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java
new file mode 100644
index 0000000..6d3cbf2
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ArchiveAllListener.java
@@ -0,0 +1,29 @@
+package nl.andrewlalis.ui.control.listeners;
+
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.view.InitializerApp;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * Listens for when the user performs an action with the intent to archive all repositories.
+ */
+public class ArchiveAllListener extends ExecutableListener {
+
+ public ArchiveAllListener(CommandExecutor executor, InitializerApp app) {
+ super(executor, app);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ String response = JOptionPane.showInputDialog(this.app, "Enter a substring to archive repositories by.", "Enter a substring", JOptionPane.QUESTION_MESSAGE);
+ if (response != null) {
+ this.executor.executeCommand("archiveall", new String[]{
+ this.app.getOrganizationName(),
+ this.app.getAccessToken(),
+ response
+ });
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java
new file mode 100644
index 0000000..d68ac1e
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/listeners/CommandFieldKeyListener.java
@@ -0,0 +1,42 @@
+package nl.andrewlalis.ui.control.listeners;
+
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+
+import javax.swing.*;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+
+/**
+ * This Key Listener listens for when the ENTER key is pressed in the command-line text field, and executes the command
+ * when that is the case.
+ */
+public class CommandFieldKeyListener implements KeyListener {
+
+ /**
+ * This is responsible for parsing and running entered commands.
+ */
+ private CommandExecutor executor;
+
+ public CommandFieldKeyListener(CommandExecutor executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public void keyTyped(KeyEvent keyEvent) {
+
+ }
+
+ @Override
+ public void keyPressed(KeyEvent keyEvent) {
+
+ }
+
+ @Override
+ public void keyReleased(KeyEvent keyEvent) {
+ if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) {
+ JTextField inputField = (JTextField) keyEvent.getComponent();
+ this.executor.executeString(inputField.getText());
+ inputField.setText(null);
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java
new file mode 100644
index 0000000..33401c0
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ExecutableListener.java
@@ -0,0 +1,29 @@
+package nl.andrewlalis.ui.control.listeners;
+
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.view.InitializerApp;
+
+import java.awt.event.ActionListener;
+
+/**
+ * An action listener which is pre-set to execute an executable once an action is performed.
+ * Since these are used for the user interface, an instance of the application is passed, for the purpose of providing
+ * a parent component for many popups, and to have access to input fields.
+ */
+public abstract class ExecutableListener implements ActionListener {
+
+ /**
+ * The executor, with some registered commands that will be executed by listeners which extend this one.
+ */
+ protected CommandExecutor executor;
+
+ /**
+ * An instance of the UI application.
+ */
+ protected InitializerApp app;
+
+ public ExecutableListener(CommandExecutor executor, InitializerApp app) {
+ this.executor = executor;
+ this.app = app;
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java
new file mode 100644
index 0000000..6143821
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/listeners/GenerateAssignmentsRepoListener.java
@@ -0,0 +1,36 @@
+package nl.andrewlalis.ui.control.listeners;
+
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.view.InitializerApp;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * Listens for when the user does an action to generate the assignments repository.
+ */
+public class GenerateAssignmentsRepoListener extends ExecutableListener {
+
+ public GenerateAssignmentsRepoListener(CommandExecutor executor, InitializerApp app) {
+ super(executor, app);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ String repoName = JOptionPane.showInputDialog(this.app, "Enter a name for the assignments repository.", "Repository Name", JOptionPane.QUESTION_MESSAGE);
+ if (repoName != null) {
+ String description = JOptionPane.showInputDialog(this.app, "Enter a description for the repository.", "Repository Description", JOptionPane.QUESTION_MESSAGE);
+ String teamName = JOptionPane.showInputDialog(this.app, "Enter the name of the TA team containing all teaching assistants.", "TA Team Name", JOptionPane.QUESTION_MESSAGE);
+ if (teamName != null) {
+ this.executor.executeCommand("generateassignments", new String[]{
+ this.app.getOrganizationName(),
+ this.app.getAccessToken(),
+ repoName,
+ description,
+ teamName
+ });
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java b/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java
new file mode 100644
index 0000000..788c431
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/control/listeners/ReadStudentsFileListener.java
@@ -0,0 +1,33 @@
+package nl.andrewlalis.ui.control.listeners;
+
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.view.InitializerApp;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * Listens for when the user performs an action to read all students from a file, and output the contents to a database.
+ */
+public class ReadStudentsFileListener extends ExecutableListener {
+
+ public ReadStudentsFileListener(CommandExecutor executor, InitializerApp app) {
+ super(executor, app);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent actionEvent) {
+ JFileChooser chooser = new JFileChooser();
+ int fileResponse = chooser.showOpenDialog(this.app);
+
+ if (fileResponse == JFileChooser.APPROVE_OPTION) {
+ String teamSizeString = JOptionPane.showInputDialog(this.app, "Enter the student team size.", "Team Size", JOptionPane.QUESTION_MESSAGE);
+ if (teamSizeString != null) {
+ this.executor.executeCommand("readstudents", new String[]{
+ chooser.getSelectedFile().getName(),
+ teamSizeString
+ });
+ }
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java
new file mode 100644
index 0000000..2ff6c2a
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/view/InitializerApp.java
@@ -0,0 +1,203 @@
+package nl.andrewlalis.ui.view;
+
+import nl.andrewlalis.ui.control.OutputTextHandler;
+import nl.andrewlalis.ui.control.command.CommandExecutor;
+import nl.andrewlalis.ui.control.command.executables.ArchiveRepos;
+import nl.andrewlalis.ui.control.listeners.ArchiveAllListener;
+import nl.andrewlalis.ui.control.listeners.CommandFieldKeyListener;
+import nl.andrewlalis.ui.control.listeners.GenerateAssignmentsRepoListener;
+import nl.andrewlalis.ui.control.listeners.ReadStudentsFileListener;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Represents the main user interface element that is referenced in main to construct the graphic interface.
+ */
+public class InitializerApp extends JFrame {
+
+ /**
+ * The window title.
+ */
+ private static final String FRAME_TITLE = "Github Initializer";
+
+ /**
+ * A default size of the window on startup.
+ */
+ private static final Dimension SIZE = new Dimension(1000, 600);
+
+ /**
+ * The pane on which general purpose program output is written.
+ */
+ private OutputTextPane outputTextPane;
+
+ private JTextField organizationField = new JTextField();
+ private JTextField accessTokenField = new JTextField();
+ private JTextField assignmentsRepoField = new JTextField();
+ private JTextField teachingAssistantsField = new JTextField();
+ private JTextField studentRepoField = new JTextField();
+
+ /**
+ * The executor responsible for performing meaningful actions.
+ */
+ private CommandExecutor executor;
+
+ public InitializerApp(CommandExecutor executor) {
+ this.executor = executor;
+
+ // UI initialization.
+ this.initFrame();
+ }
+
+ /**
+ * Begins showing the application
+ */
+ public void begin() {
+ this.pack();
+ this.setVisible(true);
+ }
+
+ /**
+ * Initializes the handler which passes logging information to the text pane for display.
+ */
+ private void initLoggingHandler() {
+ Logger logger = Logger.getGlobal();
+ OutputTextHandler handler = new OutputTextHandler(this.outputTextPane);
+ handler.setLevel(Level.FINE);
+ logger.addHandler(handler);
+ }
+
+ /**
+ * Initializes the frame before display should begin.
+ */
+ private void initFrame() {
+ this.setTitle(FRAME_TITLE);
+ this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ this.setPreferredSize(SIZE);
+
+ JPanel mainPanel = new JPanel(new BorderLayout());
+
+ mainPanel.add(this.initCommandPanel(), BorderLayout.CENTER);
+ mainPanel.add(this.initRepoPanel(), BorderLayout.WEST);
+ mainPanel.add(this.initGithubManagerPanel(), BorderLayout.EAST);
+
+ this.setContentPane(mainPanel);
+
+ this.initLoggingHandler();
+ }
+
+ /**
+ * @return A JPanel containing input for all fields needed to connect to github, plus some commonly used buttons
+ * which perform actions, as shortcuts for command actions.
+ */
+ private JPanel initGithubManagerPanel() {
+ JPanel githubManagerPanel = new JPanel(new BorderLayout());
+
+ // Information input (org name, key, etc.)
+ JPanel infoInputPanel = new JPanel();
+ infoInputPanel.setLayout(new BoxLayout(infoInputPanel, BoxLayout.PAGE_AXIS));
+
+ infoInputPanel.add(generateTextFieldPanel("Organization Name", this.organizationField));
+ this.organizationField.setText("InitializerTesting");
+ infoInputPanel.add(generateTextFieldPanel("Access Token", this.accessTokenField));
+ this.accessTokenField.setText("haha get your own");
+ infoInputPanel.add(generateTextFieldPanel("Assignments Repo Name", this.assignmentsRepoField));
+ this.assignmentsRepoField.setText("assignments_2018");
+ infoInputPanel.add(generateTextFieldPanel("TA-All Team Name", this.teachingAssistantsField));
+ this.teachingAssistantsField.setText("teaching-assistants");
+ infoInputPanel.add(generateTextFieldPanel("Student Repo Prefix", this.studentRepoField));
+ this.studentRepoField.setText("advoop_2018");
+
+ githubManagerPanel.add(infoInputPanel, BorderLayout.NORTH);
+
+ // Common actions panel.
+ JPanel commonActionsPanel = new JPanel();
+ commonActionsPanel.setLayout(new BoxLayout(commonActionsPanel, BoxLayout.PAGE_AXIS));
+
+ JButton archiveAllButton = new JButton("Archive All");
+ archiveAllButton.addActionListener(new ArchiveAllListener(this.executor, this));
+ commonActionsPanel.add(archiveAllButton);
+
+ JButton generateStudentTeamsButton = new JButton("Read teams from file");
+ generateStudentTeamsButton.addActionListener(new ReadStudentsFileListener(this.executor, this));
+ commonActionsPanel.add(generateStudentTeamsButton);
+
+ JButton generateAssignmentsRepoButton = new JButton("Generate Assignments Repo");
+ generateAssignmentsRepoButton.addActionListener(new GenerateAssignmentsRepoListener(this.executor, this));
+ commonActionsPanel.add(generateAssignmentsRepoButton);
+
+ githubManagerPanel.add(commonActionsPanel, BorderLayout.CENTER);
+
+ return githubManagerPanel;
+ }
+
+ /**
+ * @return A JPanel containing the command prompt field and output text pane.
+ */
+ private JPanel initCommandPanel() {
+ JPanel commandPanel = new JPanel(new BorderLayout());
+ // Output text pane.
+ this.outputTextPane = new OutputTextPane();
+ JScrollPane scrollPane = new JScrollPane(this.outputTextPane);
+ scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+ scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+ // Text enter field and label.
+ JPanel textEnterPanel = new JPanel(new BorderLayout());
+ textEnterPanel.setBorder(BorderFactory.createLoweredBevelBorder());
+ textEnterPanel.add(new JLabel("Command:"), BorderLayout.WEST);
+ JTextField commandField = new JTextField();
+ commandField.addKeyListener(new CommandFieldKeyListener(this.executor));
+ textEnterPanel.add(commandField, BorderLayout.CENTER);
+ // Top Label
+ JLabel commandPanelLabel = new JLabel("Program output", SwingConstants.CENTER);
+
+ commandPanel.add(scrollPane, BorderLayout.CENTER);
+ commandPanel.add(textEnterPanel, BorderLayout.SOUTH);
+ commandPanel.add(commandPanelLabel, BorderLayout.NORTH);
+ return commandPanel;
+ }
+
+ /**
+ * @return A JPanel containing a list of repositories.
+ */
+ private JPanel initRepoPanel() {
+ JPanel repoPanel = new JPanel();
+ repoPanel.setLayout(new BoxLayout(repoPanel, BoxLayout.PAGE_AXIS));
+ repoPanel.add(new JLabel("Repositories"));
+ repoPanel.add(new JList());
+ return repoPanel;
+ }
+
+ /**
+ * Generates a text input field panel.
+ * @param labelText The text for the label above the panel.
+ * @param textField A reference to the text field that is used in the panel.
+ * @return A JPanel containing the label and text field.
+ */
+ private JPanel generateTextFieldPanel(String labelText, JTextField textField) {
+ JPanel newPanel = new JPanel(new BorderLayout());
+ newPanel.add(new JLabel(labelText), BorderLayout.NORTH);
+ newPanel.add(textField);
+ newPanel.setBorder(BorderFactory.createEmptyBorder(5, 2, 5, 2));
+ return newPanel;
+ }
+
+ /**
+ * Gets the organization name entered in the relevant field.
+ * @return The organization name the user has entered.
+ */
+ public String getOrganizationName() {
+ return this.organizationField.getText().trim();
+ }
+
+ /**
+ * Gets the oauth access token entered in the relevant field.
+ * @return The access token the user has entered.
+ */
+ public String getAccessToken() {
+ return this.accessTokenField.getText().trim();
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java b/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java
new file mode 100644
index 0000000..64569cd
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/ui/view/OutputTextPane.java
@@ -0,0 +1,83 @@
+package nl.andrewlalis.ui.view;
+
+import javax.swing.*;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Style;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyledDocument;
+import java.awt.*;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A custom JTextPane object which provides easy methods to print to the screen.
+ */
+public class OutputTextPane extends JTextPane {
+
+ /**
+ * A list of named styles.
+ */
+ private Map styles;
+
+ /**
+ * A basic constructor to set default properties on this text pane.
+ */
+ public OutputTextPane() {
+ this.initStyles();
+ this.setEditable(false);
+ this.setAutoscrolls(true);
+ }
+
+ private void initStyles() {
+ this.styles = new HashMap<>();
+ Style defaultStyle = this.addStyle("default", null);
+ defaultStyle.addAttribute(StyleConstants.FontFamily, "Lucida Consonle");
+ defaultStyle.addAttribute(StyleConstants.FontSize, 12);
+ this.styles.put("default", defaultStyle);
+
+ Style grayItalics = this.addStyle("gray_italics", defaultStyle);
+ grayItalics.addAttribute(StyleConstants.Foreground, new Color(128, 128, 128));
+ grayItalics.addAttribute(StyleConstants.Italic, true);
+ this.styles.put("gray_italics", grayItalics);
+
+ Style bold = this.addStyle("bold", defaultStyle);
+ bold.addAttribute(StyleConstants.Bold, true);
+ this.styles.put("bold", bold);
+
+ Style smaller = this.addStyle("smaller", defaultStyle);
+ smaller.addAttribute(StyleConstants.FontSize, 11);
+ this.styles.put("smaller", smaller);
+ }
+
+ /**
+ * Prints some text styled with a style that is defined in initStyles.
+ * @param text The text to display.
+ * @param styleName The name of the style to use.
+ */
+ public void printStyled(String text, String styleName) {
+ StyledDocument doc = this.getStyledDocument();
+ Style style = this.styles.get(styleName);
+ if (style == null) {
+ style = this.styles.get("default");
+ }
+ try {
+ doc.insertString(doc.getLength(), text, style);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Prints a line of text, with a newline character appended at the bottom.
+ * @param text The text to append.
+ */
+ public void printLine(String text) {
+ StyledDocument doc = this.getStyledDocument();
+ try {
+ doc.insertString(doc.getLength(), (text + '\n'), null);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/util/CommandLine.java b/src/main/java/nl/andrewlalis/util/CommandLine.java
index 8e4e206..1075d9e 100644
--- a/src/main/java/nl/andrewlalis/util/CommandLine.java
+++ b/src/main/java/nl/andrewlalis/util/CommandLine.java
@@ -67,7 +67,7 @@ public class CommandLine {
options.addOption(organizationInput);
// The maximum team size.
- Option teamSizeInput = new Option("s", "teamsize", true, "The maximum size of teams to generate.");
+ Option teamSizeInput = new Option("ts", "teamsize", true, "The maximum size of teams to generate.");
teamSizeInput.setRequired(false);
options.addOption(teamSizeInput);
diff --git a/src/main/java/nl/andrewlalis/util/FileUtils.java b/src/main/java/nl/andrewlalis/util/FileUtils.java
new file mode 100644
index 0000000..972ffb1
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/util/FileUtils.java
@@ -0,0 +1,62 @@
+package nl.andrewlalis.util;
+
+import nl.andrewlalis.model.StudentTeam;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Contains some methods which come in handy in lots of other places.
+ */
+public class FileUtils {
+
+ /**
+ * The logger for outputting debug info.
+ */
+ private static final Logger logger = Logger.getLogger(FileUtils.class.getName());
+ static {
+ logger.setParent(Logger.getGlobal());
+ }
+
+ /**
+ * Reads the contents of the file specified by the filename into a String.
+ * @param filename The filename to read the file of, either relative or absolute.
+ * @return A string containing the file's contents.
+ */
+ public static String readStringFromFile(String filename) {
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(FileUtils.class.getResourceAsStream(filename)))) {
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = r.readLine()) != null) {
+ sb.append(line).append('\n');
+ }
+ return sb.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Reads a list of students from a CSV file and compiles a list of teams based on their preferred partners.
+ * @param filename The name of the CSV file.
+ * @param teamSize The intended size of teams.
+ * @return A list of student teams.
+ */
+ public static List getStudentTeamsFromCSV(String filename, int teamSize) {
+ List studentTeams;
+ try {
+ studentTeams = TeamGenerator.generateFromCSV(filename, teamSize);
+ logger.fine("Teams created:\n" + studentTeams);
+ return studentTeams;
+ } catch (IOException | ArrayIndexOutOfBoundsException e) {
+ logger.severe("Unable to generate studentTeams from CSV file, exiting. " + e.getMessage());
+ System.exit(1);
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/nl/andrewlalis/util/Logging.java b/src/main/java/nl/andrewlalis/util/Logging.java
index 625ef1e..9161693 100644
--- a/src/main/java/nl/andrewlalis/util/Logging.java
+++ b/src/main/java/nl/andrewlalis/util/Logging.java
@@ -14,17 +14,22 @@ public class Logging {
public static void setup(boolean verbose) throws IOException {
Logger logger = Logger.getGlobal();
- if (verbose) {
- logger.setLevel(Level.FINEST);
- } else {
- logger.setLevel(Level.INFO);
- }
-
- outputFile = new FileHandler("log/latest.txt");
+ outputFile = new FileHandler("log/latest.log");
formatter = new SimpleFormatter();
outputFile.setFormatter(formatter);
+ outputFile.setLevel(Level.FINEST);
+
+ if (verbose) {
+ Handler systemOut = new ConsoleHandler();
+ systemOut.setLevel(Level.ALL);
+
+ logger.addHandler(systemOut);
+ }
logger.addHandler(outputFile);
+ logger.setLevel(Level.ALL);
+
+ Logger.getLogger("").setLevel(Level.OFF);
}
diff --git a/src/main/java/nl/andrewlalis/util/TeamGenerator.java b/src/main/java/nl/andrewlalis/util/TeamGenerator.java
index fb99e71..b765e7b 100644
--- a/src/main/java/nl/andrewlalis/util/TeamGenerator.java
+++ b/src/main/java/nl/andrewlalis/util/TeamGenerator.java
@@ -1,7 +1,7 @@
package nl.andrewlalis.util;
import nl.andrewlalis.model.Student;
-import nl.andrewlalis.model.Team;
+import nl.andrewlalis.model.StudentTeam;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
@@ -32,11 +32,11 @@ public class TeamGenerator {
* @throws IOException If the file is unable to be read.
* @throws IllegalArgumentException If an invalid teamsize is given.
*/
- public static List generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException {
- logger.info("Generating teams of size " + teamSize);
+ public static List generateFromCSV(String filename, int teamSize) throws IOException, IllegalArgumentException {
+ logger.fine("Generating teams of size " + teamSize);
if (teamSize < 1) {
logger.severe("Invalid team size.");
- throw new IllegalArgumentException("Team size must be greater than or equal to 1. Got " + teamSize);
+ throw new IllegalArgumentException("StudentTeam size must be greater than or equal to 1. Got " + teamSize);
}
logger.fine("Parsing CSV file.");
Iterable records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new FileReader(filename));
@@ -46,8 +46,8 @@ public class TeamGenerator {
try {
studentMap = readAllStudents(records, teamSize);
} catch (ArrayIndexOutOfBoundsException e) {
- logger.severe("Team size does not match column count in records.");
- throw new IllegalArgumentException("Team size does not match column count in records.");
+ logger.severe("StudentTeam size does not match column count in records.");
+ throw new IllegalArgumentException("StudentTeam size does not match column count in records.");
}
@@ -72,28 +72,38 @@ public class TeamGenerator {
* @param teamSize The preferred maximum size for a team.
* @return A list of teams, most of which are of teamSize size.
*/
- private static List generateAllValidTeams(Map studentMap, int teamSize) {
+ private static List generateAllValidTeams(Map studentMap, int teamSize) {
List singleStudents = new ArrayList<>(studentMap.values());
- List teams = new ArrayList<>();
+ List studentTeams = new ArrayList<>();
int teamCount = 1;
// For each student, try to make a team from its preferred partners.
for (Map.Entry e : studentMap.entrySet()) {
- Team t = e.getValue().getPreferredTeam(studentMap);
- logger.finest("Checking if student's preferred team is valid: " + t.getStudents());
+ StudentTeam newTeam = e.getValue().getPreferredTeam(studentMap);
+ logger.finest("Checking if student's preferred team is valid:\n" + newTeam);
// Check if the team is of a valid size, and is not a duplicate.
- // Note that at this stage, singles are treated as teams of 1, and thus not valid for any teamSize > 1.
- if (t.isValid(teamSize) && !teams.contains(t)) {
- // Once we know this team is completely valid, we remove all the students in it from the list of singles.
- t.setId(teamCount++);
- singleStudents.removeAll(t.getStudents());
- teams.add(t);
- logger.fine("Created team: " + t);
+ // Note that at this stage, singles are treated as studentTeams of 1, and thus not valid for any teamSize > 1.
+ if (newTeam.isValid(teamSize)) {
+ // We know that the team is valid on its own, so now we check if it has members identical to any team already created.
+ boolean matchFound = false;
+ for (StudentTeam team : studentTeams) {
+ if (newTeam.hasSameMembers(team)) {
+ matchFound = true;
+ break;
+ }
+ }
+ if (!matchFound) {
+ // Once we know this team is completely valid, we remove all the students in it from the list of singles.
+ newTeam.setId(teamCount++);
+ singleStudents.removeAll(Arrays.asList(newTeam.getStudents()));
+ studentTeams.add(newTeam);
+ logger.fine("Created team:\n" + newTeam);
+ }
}
}
- teams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount));
- return teams;
+ studentTeams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount));
+ return studentTeams;
}
/**
@@ -104,21 +114,21 @@ public class TeamGenerator {
* @param teamIndex The current number used in assigning an id to the team.
* @return A list of teams comprising of single students.
*/
- private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) {
- List teams = new ArrayList<>();
+ private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) {
+ List studentTeams = new ArrayList<>();
while (!singleStudents.isEmpty()) {
- Team t = new Team();
+ StudentTeam t = new StudentTeam();
t.setId(teamIndex++);
- logger.fine("Creating new team of single students: " + t);
- while (t.getStudentCount() < teamSize && !singleStudents.isEmpty()) {
+ logger.fine("Creating new team of single students:\n" + t);
+ while (t.memberCount() < teamSize && !singleStudents.isEmpty()) {
Student s = singleStudents.remove(0);
logger.finest("Single student: " + s);
- t.addStudent(s);
+ t.addMember(s);
}
- teams.add(t);
- logger.fine("Created team: " + t);
+ studentTeams.add(t);
+ logger.fine("Created team:\n" + t);
}
- return teams;
+ return studentTeams;
}
/**
diff --git a/src/main/resources/sql/insert/types.sql b/src/main/resources/sql/insert/types.sql
new file mode 100644
index 0000000..3940146
--- /dev/null
+++ b/src/main/resources/sql/insert/types.sql
@@ -0,0 +1,19 @@
+INSERT INTO person_types (id, name)
+VALUES (0, 'student'),
+ (1, 'teaching-assistant'),
+ (2, 'professor');
+
+INSERT INTO team_types (id, name)
+VALUES (0, 'student_team'),
+ (1, 'teaching_assistant_team'),
+ (2, 'all_teaching_assistants'),
+ (3, 'none');
+
+INSERT INTO teams (id, team_type_id)
+VALUES (1000000, 3), -- None team for all students or TA's without a team.
+ (1000001, 2); -- Team for all teaching assistants.
+
+INSERT INTO error_types (id, name)
+VALUES (0, 'team_error'),
+ (1, 'person_error'),
+ (2, 'system_error');
\ No newline at end of file
diff --git a/src/main/resources/sql/table_init.sql b/src/main/resources/sql/table_init.sql
new file mode 100644
index 0000000..e5ffa1d
--- /dev/null
+++ b/src/main/resources/sql/table_init.sql
@@ -0,0 +1,140 @@
+PRAGMA foreign_keys = TRUE;
+PRAGMA writable_schema = 1;
+DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');
+PRAGMA writable_schema = 0;
+VACUUM;
+
+-- Basic schema design.
+CREATE TABLE IF NOT EXISTS person_types (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS persons (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ email_address TEXT NOT NULL,
+ github_username TEXT NOT NULL UNIQUE,
+ person_type_id INTEGER NOT NULL,
+ FOREIGN KEY (person_type_id)
+ REFERENCES person_types(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS team_types (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS teams (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ team_type_id INTEGER NOT NULL,
+ FOREIGN KEY (team_type_id)
+ REFERENCES team_types(id)
+ ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS teaching_assistant_teams (
+ team_id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ FOREIGN KEY (team_id)
+ REFERENCES teams(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS student_teams (
+ team_id INTEGER PRIMARY KEY,
+ repository_name TEXT,
+ group_id INTEGER NOT NULL UNIQUE,
+ teaching_assistant_team_id INTEGER,
+ FOREIGN KEY (team_id)
+ REFERENCES teams(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY (teaching_assistant_team_id)
+ REFERENCES teaching_assistant_teams(team_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS students (
+ person_id INTEGER PRIMARY KEY,
+ team_id INTEGER NOT NULL,
+ chose_partner INTEGER NOT NULL,
+ FOREIGN KEY (person_id)
+ REFERENCES persons(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY (team_id)
+ REFERENCES teams(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS student_preferred_partners (
+ student_id INTEGER PRIMARY KEY,
+ partner_id INTEGER NOT NULL,
+ FOREIGN KEY (student_id)
+ REFERENCES students(person_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ UNIQUE (student_id, partner_id)
+);
+
+CREATE TABLE IF NOT EXISTS teaching_assistants (
+ person_id INTEGER PRIMARY KEY,
+ team_id INTEGER NOT NULL,
+ FOREIGN KEY (person_id)
+ REFERENCES persons(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY (team_id)
+ REFERENCES teams(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+-- Error queue storage.
+CREATE TABLE IF NOT EXISTS error_types (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS errors (
+ id INTEGER PRIMARY KEY,
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ error_type_id INTEGER NOT NULL,
+ message TEXT NOT NULL,
+ FOREIGN KEY (error_type_id)
+ REFERENCES error_types(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS team_errors (
+ error_id INTEGER PRIMARY KEY,
+ team_id INTEGER NOT NULL,
+ FOREIGN KEY (error_id)
+ REFERENCES errors(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY (team_id)
+ REFERENCES teams(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS person_errors (
+ error_id INTEGER PRIMARY KEY,
+ person_id INTEGER NOT NULL,
+ FOREIGN KEY (error_id)
+ REFERENCES errors(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ FOREIGN KEY (person_id)
+ REFERENCES persons(id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+);
\ No newline at end of file