commit c3b1534695b297ef2db331b127905af6196e9dac Author: andrewlalis Date: Sun Jul 1 07:44:07 2018 +0200 Initial commit adding source and pom file. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..098ad5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/* +target/* +*.iml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bab1a41 --- /dev/null +++ b/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + andrewlalis + GithubInitializer + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + jar + + + + org.apache.commons + commons-csv + 1.5 + + + commons-cli + commons-cli + RELEASE + + + commons-email + commons-email + RELEASE + + + + \ No newline at end of file diff --git a/sampleAOOP.csv b/sampleAOOP.csv new file mode 100644 index 0000000..0ae3253 --- /dev/null +++ b/sampleAOOP.csv @@ -0,0 +1,6 @@ +"Timestamp","Username","Name","Student Number","Github Username","I have chosen a partner.","Your Partner's Student Number" +"2018/06/29 7:33:23 PM GMT+2","andrewlalisofficial@gmail.com","Andrew Lalis","3050831","andrewlalis","Yes","3522647" +"2018/06/29 7:50:02 PM GMT+2","klaus@student.rug.nl","Klaus Lalis","3522647","klauslalis","Yes","3050831" +"2018/06/29 7:50:53 PM GMT+2","henry@student.rug.nl","Henry Smith","3123456","henrysmith","Yes","3654321" +"2018/06/29 7:51:42 PM GMT+2","taylor@student.rug.nl","Taylor Jones","3654321","taylorjones","Yes","3123456" +"2018/06/29 9:17:10 PM GMT+2","lonely@gmail.com","Lonely Joe","3222222","lonelyjoe","No","" \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/Main.java b/src/main/java/nl/andrewlalis/Main.java new file mode 100644 index 0000000..a85ad3c --- /dev/null +++ b/src/main/java/nl/andrewlalis/Main.java @@ -0,0 +1,93 @@ +package nl.andrewlalis; + +import nl.andrewlalis.model.Team; +import nl.andrewlalis.util.TeamGenerator; +import org.apache.commons.cli.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Main program entry point. + */ +public class Main { + + public static void main(String[] args) throws IOException { + + System.out.println("Initializer for Github Repositories in Educational Organizations."); + + Map userOptions = parseArgs(args); + + List teams = TeamGenerator.generateFromCSV( + userOptions.get("input"), + Integer.parseInt(userOptions.get("teamsize")) + ); + System.out.println(teams); + + + } + + /** + * Parses the command line arguments and gets all the needed values. + * @param args The command line arguments as they are given to main. + * @return A map of keys and values for each option that the user must give. + */ + private static Map parseArgs(String[] args) { + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + Options options = setupCommandOptions(); + Map userOptions = new HashMap<>(); + try { + CommandLine cmd = parser.parse(options, args); + userOptions.put("token", cmd.getOptionValue("token")); + userOptions.put("input", cmd.getOptionValue("input")); + userOptions.put("organization", cmd.getOptionValue("organization")); + // The optional teamsize argument must be handled. + String teamSizeInput = cmd.getOptionValue("teamsize"); + System.out.println(teamSizeInput); + if (teamSizeInput == null ) { + userOptions.put("teamsize", "2"); + } else { + userOptions.put("teamsize", teamSizeInput); + } + } catch (ParseException e) { + formatter.printHelp("java -jar GithubInitializer.jar", options); + System.exit(1); + } + return userOptions; + } + + /** + * Sets up the command line interface options using Apache Commons CLI library. + * @return The Options object used when parsing the arguments. + */ + private static Options setupCommandOptions() { + Options options = new Options(); + + // Authentication token for github. + Option tokenInput = new Option("t", "token", true, "The authentication token, with which you are authenticated on Github. See the Github OAuth information section for more information."); + tokenInput.setRequired(true); + options.addOption(tokenInput); + + // CSV file of responses to form. + Option fileInput = new Option("i", "input", true, "The input file. Should be in CSV format with the following columns:\n" + + "\t Two Student numbers, two Github usernames, two email addresses, and \n" + + "\t whether the first student has a partner."); + fileInput.setRequired(true); + options.addOption(fileInput); + + // The github organization to add the repositories to. + Option organizationInput = new Option("o", "organization", true, "The name of the organization for which this program is being run."); + organizationInput.setRequired(true); + options.addOption(organizationInput); + + // The maximum team size. + Option teamSizeInput = new Option("s", "teamsize", true, "The maximum size of teams to generate."); + teamSizeInput.setRequired(false); + options.addOption(teamSizeInput); + + return options; + } +} diff --git a/src/main/java/nl/andrewlalis/model/Student.java b/src/main/java/nl/andrewlalis/model/Student.java new file mode 100644 index 0000000..d268ab5 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/Student.java @@ -0,0 +1,90 @@ +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; + + /** + * A list of partners that the student has said that they would like to be partners with. + */ + private List preferredPartners; + + public Student(int number, String name, String emailAddress, String githubUsername, List preferredPartners) { + this.number = number; + this.name = name; + this.emailAddress = emailAddress; + this.githubUsername = 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; + } + + /** + * Using a given map of all students, returns a student's preferred team. + * @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(); + for (int partnerNumber : this.getPreferredPartners()) { + t.addStudent(studentMap.get(partnerNumber)); + } + t.addStudent(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/Team.java b/src/main/java/nl/andrewlalis/model/Team.java new file mode 100644 index 0000000..bd82bd2 --- /dev/null +++ b/src/main/java/nl/andrewlalis/model/Team.java @@ -0,0 +1,147 @@ +package nl.andrewlalis.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents one or more students' collective information. + */ +public class Team { + + /** + * The list of students in this team. + */ + private List students; + + /** + * The team identification number. + */ + private int id; + + public Team() { + this.students = new ArrayList<>(); + this.id = -1; + } + + /** + * 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. + */ + public boolean hasStudent(Student student) { + for (Student s : this.students) { + if (s.equals(student)) { + 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; + } + + /** + * Adds a student to this team. + * @param student The student to add. + * @return True if the student could be added, false otherwise. + */ + public boolean addStudent(Student student) { + if (!this.hasStudent(student)) { + this.students.add(student); + return true; + } else { + return false; + } + } + + /** + * 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.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; + } + } + + /** + * 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.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)) { + return false; + } + } + return true; + } else { + return false; + } + } +} diff --git a/src/main/java/nl/andrewlalis/util/TeamGenerator.java b/src/main/java/nl/andrewlalis/util/TeamGenerator.java new file mode 100644 index 0000000..f88606f --- /dev/null +++ b/src/main/java/nl/andrewlalis/util/TeamGenerator.java @@ -0,0 +1,79 @@ +package nl.andrewlalis.util; + +import nl.andrewlalis.model.Student; +import nl.andrewlalis.model.Team; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; + +import java.io.FileReader; +import java.io.IOException; +import java.util.*; + +public class TeamGenerator { + + public static List generateFromCSV(String filename, int teamSize) throws IOException { + System.out.println("Generating teams of size " + teamSize); + Iterable records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new FileReader(filename)); + + Map studentMap = readAllStudents(records, teamSize); + return generateAllValidTeams(studentMap, teamSize); + } + + private static List generateAllValidTeams(Map studentMap, int teamSize) { + List singleStudents = new ArrayList<>(studentMap.values()); + List teams = 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); + // 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)) { + t.setId(teamCount++); + // Once we know this team is completely valid, we remove all the students in it from the list of singles. + singleStudents.removeAll(t.getStudents()); + teams.add(t); + } + } + + teams.addAll(mergeSingleStudents(singleStudents, teamSize, teamCount)); + return teams; + } + + private static List mergeSingleStudents(List singleStudents, int teamSize, int teamIndex) { + List teams = new ArrayList<>(); + while (!singleStudents.isEmpty()) { + Team t = new Team(); + t.setId(teamIndex++); + while (t.getStudentCount() < teamSize && !singleStudents.isEmpty()) { + t.addStudent(singleStudents.remove(0)); + } + teams.add(t); + } + return teams; + } + + /** + * Reads all the rows from the CSV file, and returns a map of students, using student number as the index. + * @param records The records in the CSV file. + * @param teamSize The preferred size of teams, or rather, the expected number of partners. + * @return A map of all students in the file. + */ + private static Map readAllStudents(Iterable records, int teamSize) { + Map studentMap = new HashMap<>(); + for (CSVRecord record : records) { + List preferredIds = new ArrayList<>(); + if (record.get(5).equals("Yes")) { + int columnOffset = 6; + for (int i = 0; i < teamSize-1; i++) { + preferredIds.add(Integer.parseInt(record.get(columnOffset + i))); + } + } + Student s = new Student(Integer.parseInt(record.get(3)), record.get(2), record.get(1), record.get(4), preferredIds); + studentMap.put(s.getNumber(), s); + } + return studentMap; + } + +}