diff --git a/build.gradle b/build.gradle index fbdd30592c..b258772071 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ test { } application { - mainClass.set("dude.Dude") + mainClass.set("dude.Launcher") } shadowJar { diff --git a/src/main/java/dude/Dude.java b/src/main/java/dude/Dude.java index a76bf44682..8b28f385de 100644 --- a/src/main/java/dude/Dude.java +++ b/src/main/java/dude/Dude.java @@ -1,16 +1,12 @@ package dude; import java.io.IOException; -import java.util.NoSuchElementException; -import java.util.Scanner; import dude.commands.Command; -import dude.commands.CommandTypes; import dude.commands.Parser; import dude.exceptions.DudeException; import dude.tasks.TaskList; import dude.utils.Storage; -import dude.utils.Ui; /** @@ -22,12 +18,12 @@ * parsing it into a command, executing the command and saving the task list to disk. **/ public class Dude { + private static final String STORAGE_LOADING_ERROR_MESSAGE = "An error occurred while loading the tasks. " + + "Deleting the storage and starting with an empty task list."; + private static final String STORAGE_SAVING_ERROR_MESSAGE = "An error occurred while saving the tasks to disk."; private final TaskList taskList; private final Storage storage; - private final Ui ui; - private boolean isRunning = true; - /** * Constructor for the Dude class. *

@@ -38,72 +34,27 @@ public class Dude { */ public Dude(String filePath) { this.storage = new Storage(filePath); - this.ui = new Ui(); - - TaskList temp = null; - try { - temp = this.storage.loadTasks(); - } catch (Exception e) { //Thrown when file gets corrupted - System.out.println("An error occurred while loading the tasks. Deleting the storage and starting with " - + "an empty task list."); - this.storage.deleteStorage(); - temp = new TaskList(); - } - - this.taskList = temp; + this.taskList = loadTaskList(); } + /** + * The main method used to interact with dude. User input is passed to this method to execute the + * appropriate command and send back a response from dude. + *

+ * + * @param input The user input to be processed. + */ public String getResponse(String input) { Command c = Parser.parse(input, taskList); String response = executeCommand(c); try { - saveToDisk(); + saveTaskListToDisk(); } catch (IOException e) { - return "An error occurred while saving the tasks to disk."; + return STORAGE_SAVING_ERROR_MESSAGE; } return response; } - /** - * This method runs the main loop of the application. - *

- * This method is responsible for reading user input, parsing it into a command, - * executing the command and saving the task list to disk. - */ - public void run() { - - ui.showWelcome(); - Scanner sc = new Scanner(System.in); - while (this.isRunning) { - String input = extractInput(sc); - Command command = Parser.parse(input, taskList); - - String response = executeCommand(command); - ui.showMessage(response); - - try { - saveToDisk(); - } catch (IOException e) { - System.out.println("An error occurred while saving the tasks to disk."); - } - - if (command.getCommandType() == CommandTypes.BYE) { - this.isRunning = false; - } - } - } - - private static String extractInput(Scanner sc) { - String input = ""; - try { - input = sc.nextLine(); - } catch (NoSuchElementException e) { - //this will not be handled. App will only exit at bye command. - input = ""; - } - return input; - } - private static String executeCommand(Command command) { try { return command.execute(); @@ -112,8 +63,20 @@ private static String executeCommand(Command command) { } } - private void saveToDisk() throws IOException, SecurityException { + private void saveTaskListToDisk() throws IOException, SecurityException { this.storage.saveTasks(taskList); } + private TaskList loadTaskList() { + TaskList temp; + try { + temp = this.storage.loadTasks(); + } catch (Exception e) { //Thrown when file gets corrupted + System.out.println(STORAGE_LOADING_ERROR_MESSAGE); + this.storage.deleteStorage(); + temp = new TaskList(); + } + return temp; + } + } diff --git a/src/main/java/dude/Launcher.java b/src/main/java/dude/Launcher.java index 97dd1dafc9..ec85912d2e 100644 --- a/src/main/java/dude/Launcher.java +++ b/src/main/java/dude/Launcher.java @@ -2,8 +2,16 @@ import javafx.application.Application; +/** + * A Launcher class that acts as the entry point for the application. + */ public class Launcher { + /** + * Launches the application. + * + * @param args The command line arguments. + */ public static void main(String[] args) { Application.launch(Main.class, args); } diff --git a/src/main/java/dude/Main.java b/src/main/java/dude/Main.java index 7baf14ade9..1ae8e5c750 100644 --- a/src/main/java/dude/Main.java +++ b/src/main/java/dude/Main.java @@ -1,20 +1,40 @@ package dude; +import java.io.IOException; + import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; -import java.io.IOException; - +/** + * The Main class is used to boot up the GUI for the application. + */ public class Main extends Application { + /** + * Method caled to start the application. It initializes the UI and sets up the javafx stage + * + * @param stage The stage to be used for the application. + */ @Override public void start(Stage stage) { stage.setTitle("Dude"); stage.setResizable(false); + Scene scene = loadMainScene(); + stage.setScene(scene); + + stage.show(); + } + + /** + * Loads the main scene of the application from the FXML file. + * + * @return The scene that is loaded. + */ + private Scene loadMainScene() { FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainView.fxml")); AnchorPane anchorPane = null; try { @@ -22,9 +42,8 @@ public void start(Stage stage) { } catch (IOException e) { throw new RuntimeException(e); } - Scene scene = new Scene(anchorPane); - stage.setScene(scene); - stage.show(); + return scene; } } + diff --git a/src/main/java/dude/DialogBox.java b/src/main/java/dude/gui/DialogBox.java similarity index 92% rename from src/main/java/dude/DialogBox.java rename to src/main/java/dude/gui/DialogBox.java index c120231ffd..d0147e4700 100644 --- a/src/main/java/dude/DialogBox.java +++ b/src/main/java/dude/gui/DialogBox.java @@ -1,25 +1,27 @@ -package dude; +package dude.gui; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.fxml.FXML; import java.io.IOException; import java.util.Collections; import javafx.collections.FXCollections; import javafx.collections.ObservableList; - +import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +// @@author Jeffry Lum +// Solution below is reused from https://se-education.org/guides/tutorials/javaFxPart4.html /** - * An example of a custom control using FXML. * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label * containing text from the speaker. + * */ public class DialogBox extends HBox { @FXML diff --git a/src/main/java/dude/MainView.java b/src/main/java/dude/gui/MainView.java similarity index 55% rename from src/main/java/dude/MainView.java rename to src/main/java/dude/gui/MainView.java index ac95d82d3b..3508e85e0f 100644 --- a/src/main/java/dude/MainView.java +++ b/src/main/java/dude/gui/MainView.java @@ -1,5 +1,6 @@ -package dude; +package dude.gui; +import dude.Dude; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; @@ -12,6 +13,8 @@ * Controller for MainView. Provides the layout for the other controls. */ public class MainView extends AnchorPane { + private static final String USER_IMAGE_PATH = "/images/user.png"; + private static final String DUDE_IMAGE_PATH = "/images/dude.png"; @FXML private ScrollPane scrollPane; @@ -24,37 +27,50 @@ public class MainView extends AnchorPane { @FXML private VBox dialogContainer; - private Image userImage = new Image(this.getClass().getResourceAsStream("/images/user.png")); - private Image dudeImage = new Image(this.getClass().getResourceAsStream("/images/dude.png")); - + private Image userImage; + private Image dudeImage; private Dude dude; + /** + * Initializes the MainView, setting up the images and the Dude object. + */ @FXML public void initialize() { - dude = new Dude("data/tasks.ser"); + this.dude = new Dude("data/tasks.ser"); + this.userImage = new Image(this.getClass().getResourceAsStream(USER_IMAGE_PATH)); + this.dudeImage = new Image(this.getClass().getResourceAsStream(DUDE_IMAGE_PATH)); } + /** + * Creates two dialog boxes, one echoing user input and the other containing Dude's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ @FXML public void handleUserInput() { String input = userInputField.getText(); - String response = dude.getResponse(input); + if (input.equals("bye")) { + System.exit(0); + } - System.out.println("User input: " + input); + String response = dude.getResponse(input); + ; userInputField.clear(); - dialogContainer.getChildren().addAll( - DialogBox.getUserDialog(input, userImage), - DialogBox.getDukeDialog(response, dudeImage) - ); + showInputAndResponse(input, response); + scrollDown(); + } + private void scrollDown() { dialogContainer.heightProperty().addListener((observable) -> { scrollPane.setVvalue(1.0); }); + } - if (input.equals("bye")) { - System.exit(0); - } - + private void showInputAndResponse(String input, String response) { + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dudeImage) + ); } } diff --git a/src/main/java/dude/tasks/Deadline.java b/src/main/java/dude/tasks/Deadline.java index e8a560035e..b94b70aeda 100644 --- a/src/main/java/dude/tasks/Deadline.java +++ b/src/main/java/dude/tasks/Deadline.java @@ -44,34 +44,30 @@ public Deadline(String description, LocalDateTime by) { */ public static Deadline from(String s) throws InvalidFormatException, InvalidDescriptionException, InvalidArgumentException { - assert (s != null); - - //get rid of the command String rest = Utils.discardFirstWord(s.trim()).trim(); - String[] arr = rest.split(" "); - int byOccurences = Utils.countOccurrences(arr, "/by"); + validateBy(arr); + int byIndex = Utils.findIndex(arr, "/by"); - if (byOccurences == 0 || byOccurences > 1) { - throw new InvalidFormatException("deadline", "format: deadline /by . " - + "Provide one and only one '/by'."); - } + String description = extractDescription(byIndex, arr); + String by = extractByString(byIndex, arr); + LocalDateTime dt = extractDateFromString(by); - //they will not be -1 as I have already checked for their occurences - int byIndex = Utils.findIndex(arr, "/by"); + return new Deadline(description, dt); + } - //description is from 0 to byIndex - String description = ""; - for (int i = 0; i < byIndex; i++) { - description += arr[i] + " "; - } - description = description.trim(); - if (description.isEmpty()) { - throw new InvalidDescriptionException("The description of a deadline cannot be empty."); - } + /** + * Returns the deadline of the Deadline object. + * + * @return The deadline date-time of the Deadline object. + */ + public LocalDateTime getByTime() { + return deadlineDate; + } + private static String extractByString(int byIndex, String[] arr) throws InvalidArgumentException { String by = ""; for (int i = byIndex + 1; i < arr.length; i++) { by += arr[i] + " "; @@ -81,25 +77,41 @@ public static Deadline from(String s) throws InvalidFormatException, throw new InvalidArgumentException("The 'by' of a deadline cannot be empty. " + "Follow this format: deadline /by "); } + return by; + } + private static LocalDateTime extractDateFromString(String by) throws InvalidFormatException { try { LocalDateTime dt = parseDate(by); - return new Deadline(description, dt); + return dt; } catch (DateTimeParseException e) { throw new InvalidFormatException("Invalid date format after '/by'. " + "Use d/M/yyyy or d/M/yyy H:m in 24-hour format"); } } - /** - * Returns the deadline of the Deadline object. - * - * @return The deadline date-time of the Deadline object. - */ - public LocalDateTime getBy() { - return deadlineDate; + private static String extractDescription(int byIndex, String[] arr) throws InvalidDescriptionException { + String description = ""; + for (int i = 0; i < byIndex; i++) { + description += arr[i] + " "; + } + + description = description.trim(); + if (description.isEmpty()) { + throw new InvalidDescriptionException("The description of a deadline cannot be empty."); + } + return description; + } + + private static void validateBy(String[] arr) throws InvalidFormatException { + int byOccurences = Utils.countOccurrences(arr, "/by"); + if (byOccurences == 0 || byOccurences > 1) { + throw new InvalidFormatException("deadline", "format: deadline /by . " + + "Provide one and only one '/by'."); + } } + /** * Returns a string representation of the Deadline object. * @@ -120,7 +132,7 @@ public String toString() { public boolean equals(Object object) { if (object instanceof Deadline) { Deadline t = (Deadline) object; - return t.getDescription().equals(this.getDescription()) && t.getBy().equals(this.getBy()); + return t.getDescription().equals(this.getDescription()) && t.getByTime().equals(this.getByTime()); } return false; } diff --git a/src/main/java/dude/tasks/Event.java b/src/main/java/dude/tasks/Event.java index 5bece187aa..11249ccda2 100644 --- a/src/main/java/dude/tasks/Event.java +++ b/src/main/java/dude/tasks/Event.java @@ -53,61 +53,82 @@ public static Event from(String input) throws InvalidArgumentException, String rest = Utils.discardFirstWord(input.trim()).trim(); String[] arr = rest.split(" "); - int fromOccurrences = Utils.countOccurrences(arr, "/from"); - if (fromOccurrences == 0 || fromOccurrences > 1) { - throw new InvalidFormatException("Invalid format. Follow this format :" + EventCommand.COMMAND_FORMAT - + ". Provide one and only one '/from'."); - } + validateKeywordOccurences(arr); - int toOccurrences = Utils.countOccurrences(arr, "/to"); - if (toOccurrences == 0 || toOccurrences > 1) { - throw new InvalidFormatException("Invalid format. Follow this format: " + EventCommand.COMMAND_FORMAT - + ". Provide one and only one '/to'."); - } - - //they will not be -1 as I have already checked for their occurences int fromIndex = Utils.findIndex(arr, "/from"); int toIndex = Utils.findIndex(arr, "/to"); - if (fromIndex > toIndex) { throw new InvalidFormatException("The 'from time' of an event cannot be after the 'to time'."); } - //description is from 0 to fromIndex - String description = ""; - for (int i = 0; i < fromIndex; i++) { - description += arr[i] + " "; + String description = extractDescription(fromIndex, arr); + String fromTime = extractFromString(fromIndex, toIndex, arr); + String toTime = extractToTimeString(toIndex, arr); + + Event event = getEvent(fromTime, toTime, description); + return event; + } + + private static Event getEvent(String fromTime, String toTime, String description) throws InvalidFormatException { + Event event = null; + try { + LocalDateTime from = parseDate(fromTime); + LocalDateTime to = parseDate(toTime); + event = new Event(description, from, to); + } catch (DateTimeParseException e) { + throw new InvalidFormatException("Invalid date format after '/from' or '/to'." + + "Use d/M/yyyy or d/M/yyy H:m in 24-hour format"); } - description = description.trim(); - if (description.isEmpty()) { - throw new InvalidDescriptionException("The description of an event cannot be empty."); + return event; + } + + private static String extractToTimeString(int toIndex, String[] arr) throws InvalidArgumentException { + String toTime = ""; + for (int i = toIndex + 1; i < arr.length; i++) { + toTime += arr[i] + " "; } + toTime = toTime.trim(); + if (toTime.isEmpty()) { + throw new InvalidArgumentException("The '/to' of an event cannot be empty."); + } + return toTime; + } + private static String extractFromString(int fromIndex, int toIndex, String[] arr) throws InvalidArgumentException { String fromTime = ""; for (int i = fromIndex + 1; i < toIndex; i++) { fromTime += arr[i] + " "; } fromTime = fromTime.trim(); if (fromTime.isEmpty()) { - throw new InvalidArgumentException("The 'fromTime' of an event cannot be empty."); + throw new InvalidArgumentException("The '/from' of an event cannot be empty."); } + return fromTime; + } - String toTime = ""; - for (int i = toIndex + 1; i < arr.length; i++) { - toTime += arr[i] + " "; + private static String extractDescription(int fromIndex, String[] arr) throws InvalidDescriptionException { + String description = ""; + for (int i = 0; i < fromIndex; i++) { + description += arr[i] + " "; } - toTime = toTime.trim(); - if (toTime.isEmpty()) { - throw new InvalidArgumentException("The 'toTime' of an event cannot be empty."); + description = description.trim(); + if (description.isEmpty()) { + throw new InvalidDescriptionException("The description of an event cannot be empty."); } + return description; + } - try { - LocalDateTime from = parseDate(fromTime); - LocalDateTime to = parseDate(toTime); - return new Event(description, from, to); - } catch (DateTimeParseException e) { - throw new InvalidFormatException("Invalid date format after '/from' or '/to'." - + "Use d/M/yyyy or d/M/yyy H:m in 24-hour format"); + private static void validateKeywordOccurences(String[] arr) throws InvalidFormatException { + int fromOccurrences = Utils.countOccurrences(arr, "/from"); + if (fromOccurrences == 0 || fromOccurrences > 1) { + throw new InvalidFormatException("Invalid format. Follow this format :" + EventCommand.COMMAND_FORMAT + + ". Provide one and only one '/from'."); + } + + int toOccurrences = Utils.countOccurrences(arr, "/to"); + if (toOccurrences == 0 || toOccurrences > 1) { + throw new InvalidFormatException("Invalid format. Follow this format: " + EventCommand.COMMAND_FORMAT + + ". Provide one and only one '/to'."); } } diff --git a/src/main/java/dude/tasks/Task.java b/src/main/java/dude/tasks/Task.java index 77faaa47d4..00d9fc07c8 100644 --- a/src/main/java/dude/tasks/Task.java +++ b/src/main/java/dude/tasks/Task.java @@ -10,6 +10,8 @@ * The Task class represents a task with a description and a status. */ public class Task implements Serializable { + private static final String DATE_TIME_FORMAT = "d/M/yyyy H:m"; + private static final String DATE_FORMAT = "d/M/yyyy"; private final String description; private boolean isDone; @@ -74,12 +76,11 @@ protected static LocalDateTime parseDate(String string) throws DateTimeParseExce String dateTimePattern = "\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{2}"; String datePattern = "\\d{1,2}/\\d{1,2}/\\d{4}"; - if (string.matches(dateTimePattern)) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/M/yyyy H:m"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT); return LocalDateTime.parse(string, formatter); } else if (string.matches(datePattern)) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d/M/yyyy"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); LocalDate date = LocalDate.parse(string, formatter); return date.atStartOfDay(); } else { diff --git a/src/main/java/dude/tasks/TaskList.java b/src/main/java/dude/tasks/TaskList.java index 873bb276cc..5ad8e3d6aa 100644 --- a/src/main/java/dude/tasks/TaskList.java +++ b/src/main/java/dude/tasks/TaskList.java @@ -134,9 +134,11 @@ public ArrayList getList() { ArrayList copy = new ArrayList<>(); for (Task task : list) { if (task instanceof Deadline) { - copy.add(new Deadline(task.getDescription(), ((Deadline) task).getBy())); + Deadline deadline = (Deadline) task; + copy.add(new Deadline(deadline.getDescription(), deadline.getByTime())); } else if (task instanceof Event) { - copy.add(new Event(task.getDescription(), ((Event) task).getFromTime(), ((Event) task).getToTime())); + Event event = (Event) task; + copy.add(new Event(event.getDescription(), event.getFromTime(), event.getToTime())); } else if (task instanceof Todo) { copy.add(new Todo(task.getDescription())); } else { @@ -177,7 +179,7 @@ public Task getTask(int taskID) throws IndexOutOfBoundsException { */ @Override public String toString() { - if (list.size() == 0) { + if (list.isEmpty()) { return "No tasks in the list! :("; } diff --git a/src/main/java/dude/tasks/Todo.java b/src/main/java/dude/tasks/Todo.java index 79f1d4a347..b2d4bfabe1 100644 --- a/src/main/java/dude/tasks/Todo.java +++ b/src/main/java/dude/tasks/Todo.java @@ -27,15 +27,12 @@ public Todo(String description) { * @throws InvalidDescriptionException if the description of the todo is empty. */ public static Todo from(String s) throws InvalidDescriptionException { - - //get rid of the command String description = Utils.discardFirstWord(s.trim()).trim(); - - if (!description.isEmpty()) { - return new Todo(description); - } else { + if (description.isEmpty()) { throw new InvalidDescriptionException("The description of a todo cannot be empty."); } + + return new Todo(description); } /** diff --git a/src/main/resources/view/MainView.fxml b/src/main/resources/view/MainView.fxml index 6093d4bf1e..2cac3eefad 100644 --- a/src/main/resources/view/MainView.fxml +++ b/src/main/resources/view/MainView.fxml @@ -11,7 +11,7 @@ + xmlns:fx="http://javafx.com/fxml/1" fx:controller="dude.gui.MainView">