diff --git a/src/main/java/nl/andrewlalis/Main.java b/src/main/java/nl/andrewlalis/Main.java index ce4ea6e..feb9ace 100644 --- a/src/main/java/nl/andrewlalis/Main.java +++ b/src/main/java/nl/andrewlalis/Main.java @@ -1,20 +1,19 @@ package nl.andrewlalis; +import nl.andrewlalis.database.Database; import nl.andrewlalis.git_api.GithubManager; +import nl.andrewlalis.model.Student; import nl.andrewlalis.model.StudentTeam; +import nl.andrewlalis.util.CommandLine; +import nl.andrewlalis.util.FileUtils; import nl.andrewlalis.util.Logging; import nl.andrewlalis.util.TeamGenerator; import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; import java.util.logging.Logger; -import nl.andrewlalis.util.CommandLine; - /** * Main program entry point. */ @@ -28,16 +27,9 @@ public class Main { Map userOptions = CommandLine.parseArgs(args); // Initialize logger. - ConsoleHandler handler = new ConsoleHandler(); - handler.setLevel(Level.INFO); try { Logging.setup(true); // TODO: Replace true with command line arg. - Handler[] handlers = logger.getHandlers(); - for (Handler h : handlers) { - logger.removeHandler(h); - } - logger.setUseParentHandlers(false); - logger.addHandler(handler); + } catch (IOException e) { logger.severe("Unable to save log to file."); } @@ -62,6 +54,16 @@ public class Main { e.printStackTrace(); } + // Initialize database. + Database db = new Database("database/initializer.sqlite"); + db.initialize(); + for (StudentTeam team : studentTeams) { + for (Student student : team.getStudents()) { + db.storeStudent(student); + } + } + + } /** diff --git a/src/main/java/nl/andrewlalis/database/Database.java b/src/main/java/nl/andrewlalis/database/Database.java new file mode 100644 index 0000000..14a7f1c --- /dev/null +++ b/src/main/java/nl/andrewlalis/database/Database.java @@ -0,0 +1,159 @@ +package nl.andrewlalis.database; + +import nl.andrewlalis.model.Person; +import nl.andrewlalis.model.Student; +import nl.andrewlalis.model.TeachingAssistant; +import nl.andrewlalis.util.FileUtils; + +import java.sql.*; +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 = 0; + private static final int TEAM_TA_ALL = 1; + + 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 and starting data. + * @return True, if successful, false if not. + */ + public boolean initialize() { + String sql = FileUtils.readStringFromFile("/sql/table_init.sql"); + String[] commands = sql.split(";"); + for (String command : commands) { + logger.finest("Executing command: " + command); + if (command.trim().length() > 1) { + try { + PreparedStatement statement = this.connection.prepareStatement(command); + statement.execute(); + } catch (SQLException e) { + logger.severe("SQLException: " + e.getErrorCode()); + return false; + } + } + } + logger.fine("Database initialized."); + 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 { + 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); + return stmt.execute(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 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); + return stmt.execute(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 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) { + 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); + return stmt.execute(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/git_api/GithubManager.java b/src/main/java/nl/andrewlalis/git_api/GithubManager.java index 882658c..101d1f1 100644 --- a/src/main/java/nl/andrewlalis/git_api/GithubManager.java +++ b/src/main/java/nl/andrewlalis/git_api/GithubManager.java @@ -14,7 +14,6 @@ import org.kohsuke.github.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.logging.Logger; /** @@ -206,21 +205,30 @@ public class GithubManager { List repositories = this.organization.listRepositories().asList(); for (GHRepository repo : repositories) { if (repo.getName().contains(sub)) { - 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()); - // TODO: archive repository using Github Java API, instead of Apache HttpUtils. + 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()); + } + } diff --git a/src/main/java/nl/andrewlalis/model/Student.java b/src/main/java/nl/andrewlalis/model/Student.java index 1cb0474..321c4e4 100644 --- a/src/main/java/nl/andrewlalis/model/Student.java +++ b/src/main/java/nl/andrewlalis/model/Student.java @@ -20,7 +20,7 @@ public class Student extends Person { * @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. + * other students' numbers. */ public Student(int number, String name, String emailAddress, String githubUsername, List preferredPartners) { super(number, name, emailAddress, githubUsername); 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..809d2bd --- /dev/null +++ b/src/main/java/nl/andrewlalis/util/FileUtils.java @@ -0,0 +1,31 @@ +package nl.andrewlalis.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Contains some methods which come in handy in lots of other places. + */ +public class FileUtils { + + /** + * 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; + } + } + +} diff --git a/src/main/java/nl/andrewlalis/util/Logging.java b/src/main/java/nl/andrewlalis/util/Logging.java index 625ef1e..1a021ea 100644 --- a/src/main/java/nl/andrewlalis/util/Logging.java +++ b/src/main/java/nl/andrewlalis/util/Logging.java @@ -14,16 +14,26 @@ 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); + Handler[] handlers = logger.getHandlers(); + for (Handler h : handlers) { + logger.removeHandler(h); } + logger.setUseParentHandlers(false); + + ConsoleHandler handler = new ConsoleHandler(); + if (verbose) { + handler.setLevel(Level.FINEST); + } else { + handler.setLevel(Level.INFO); + } + + logger.addHandler(handler); outputFile = new FileHandler("log/latest.txt"); formatter = new SimpleFormatter(); outputFile.setFormatter(formatter); + outputFile.setLevel(Level.FINEST); logger.addHandler(outputFile); } diff --git a/src/main/resources/sql/insert/insert_person.sql b/src/main/resources/sql/insert/insert_person.sql new file mode 100644 index 0000000..71b6034 --- /dev/null +++ b/src/main/resources/sql/insert/insert_person.sql @@ -0,0 +1,2 @@ +INSERT INTO persons (id, name, email_address, github_username, person_type_id) +VALUES (?, ?, ?, ?, ?); \ No newline at end of file diff --git a/src/main/resources/sql/insert/insert_student.sql b/src/main/resources/sql/insert/insert_student.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/sql/table_init.sql b/src/main/resources/sql/table_init.sql new file mode 100644 index 0000000..efc6cf0 --- /dev/null +++ b/src/main/resources/sql/table_init.sql @@ -0,0 +1,150 @@ +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 +); + +INSERT INTO person_types (id, name) +VALUES (0, 'student'), + (1, 'teaching-assistant'), + (2, 'professor'); + +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 +); + +INSERT INTO team_types (id, name) +VALUES (0, 'student_team'), + (1, 'teaching_assistant_team'), + (2, 'all_teaching_assistants'), + (3, 'none'); + +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 +); + +INSERT INTO teams (id, team_type_id) +VALUES (0, 3), -- None team for all students or TA's without a team. + (1, 2); -- Team for all teaching assistants. + +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 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 +); + +INSERT INTO error_types (id, name) +VALUES (0, 'team_error'), + (1, 'person_error'), + (2, 'system_error'); + +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