diff --git a/.gitignore b/.gitignore index 2873e189e..c83f1a88b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +# Bot data files +/data/ + +# Maven build files +/target/ \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8077118eb..98ee971c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,69 @@ -# User Guide +# XiaoAi User Guide ## Features -### Feature-ABC +### Feature: Add tasks -Description of the feature. +You can add your tasks to the task list. -### Feature-XYZ +### Feature: Delete tasks -Description of the feature. +You can delete an existing task from the task list. + +### Feature: List tasks + +You can list all existing tasks. + +### Feature: Mark task as done/not done + +You can mark an existing task as done/not done. + +### Feature: Find tasks + +You can search for tasks using keywords. ## Usage -### `Keyword` - Describe action +### `todo` - adds a todo task + +usage: `todo ` +example usage: `todo drink some coffee` + +### `deadline` - adds a deadline task + +usage: `deadline -by ` +example usage: `deadline submit assignment -by today 7pm` + +### `event` - adds an event task + +usage: `event -from -to ` +example usage: `event go to concert -from 11/7 8pm -to 11/7 10pm` + +### `list` - list all existing tasks + +usage: `list` + +### `delete` - delete a task with index + +usage: `delete ` +example usage: `delete 3` + +### `mark` - mark a task as done + +usage: `mark ` +example usage: `mark 3` + +### `unmark` - mark a task as not done -Describe the action and its outcome. +usage: `unmark ` +example usage: `unmark 3` -Example of usage: +### `find` - find all tasks with name containing the keywords -`keyword (optional arguments)` +usage: `find ` +example usage: `find read books` -Expected outcome: +### `bye` - stop the bot and quit -Description of the outcome. +usage: `bye` -``` -expected output -``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..0c5e16ab1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + cn.yfshadaow.cs2113.ip + XiaoAiBot + 1.0.0 + jar + + XiaoAiBot + + + 11 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + + cn.yfshadaow.cs2113.ip.Duke + 1.0 + + + + false + + + + + + org.apache.maven.plugins + maven-help-plugin + 3.2.0 + + + install + src/main/java + + + src/main/resources + true + + + + + + + + com.google.code.gson + gson + 2.10.1 + + + + com.google.errorprone + error_prone_annotations + 2.22.0 + + + + commons-io + commons-io + 2.14.0 + + + + diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334c..000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/Duke.java b/src/main/java/cn/yfshadaow/cs2113/ip/Duke.java new file mode 100644 index 000000000..6b0b80acd --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/Duke.java @@ -0,0 +1,17 @@ +package cn.yfshadaow.cs2113.ip; + +/** + * The main class for the application + */ +public class Duke { + + /** + * The entry point of application. + * + * @param args the input arguments + */ + public static void main(String[] args) { + XiaoAiBot bot = new XiaoAiBot(); + bot.start(); + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/XiaoAiBot.java b/src/main/java/cn/yfshadaow/cs2113/ip/XiaoAiBot.java new file mode 100644 index 000000000..f94b41d6e --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/XiaoAiBot.java @@ -0,0 +1,117 @@ +package cn.yfshadaow.cs2113.ip; + +import cn.yfshadaow.cs2113.ip.command.Command; +import cn.yfshadaow.cs2113.ip.command.CommandHandler; +import cn.yfshadaow.cs2113.ip.storage.Storage; +import cn.yfshadaow.cs2113.ip.ui.Ui; +import cn.yfshadaow.cs2113.ip.utils.Parser; +import cn.yfshadaow.cs2113.ip.utils.TaskList; +import com.google.gson.JsonParseException; + +import java.io.IOException; + +/** + * Represents a bot. + */ +public class XiaoAiBot { + + private final Storage storage = new Storage(); + + /** + * Gets storage. + * + * @return the storage + */ + @SuppressWarnings("unused") + public Storage getStorage() { + return storage; + } + + private final Ui ui = new Ui(this); + + /** + * Gets ui. + * + * @return the ui + */ + @SuppressWarnings("unused") + public Ui getUi() { + return ui; + } + + private final TaskList taskList = new TaskList(); + + /** + * Gets task list. + * + * @return the task list + */ + @SuppressWarnings("unused") + public TaskList getTaskList() { + return taskList; + } + + + /** + * Gets command handler. + * + * @return the command handler + */ + @SuppressWarnings("unused") + public CommandHandler getCommandHandler() { + return commandHandler; + } + + private final CommandHandler commandHandler = new CommandHandler(this); + + /** + * Sets whether the bot should quit. + * + * @param shouldQuit whether the bot should quit + */ + public void setShouldQuit(boolean shouldQuit) { + this.shouldQuit = shouldQuit; + } + + private boolean shouldQuit = false; + + private void initialize() throws IOException{ + storage.initialize(); + try { + taskList.tasks.addAll(storage.loadData().tasks); + ui.sendMessage("Successfully loaded data from data file"); + } catch (JsonParseException e) { + ui.sendMessage(String.format("Failed to load data becauseJSON parsing failed: %s", e.getMessage())); + } catch (Exception e) { + ui.sendMessage(String.format("Failed to load data from data file: %s", e.getMessage())); + } + } + + + /** + * Start the bot. + */ + public void start() { + try { + initialize(); + } catch (Exception e) { + ui.sendMessage(String.format("Bot initialization failed: %s", e.getMessage())); + return; + } + ui.greet(); + + while (!shouldQuit) { + String commandString = ui.readLine(); + Command cmd; + try { + cmd = Parser.parseCommand(commandString); + } catch (IllegalArgumentException e) { + ui.sendMessage(String.format("Error parsing command: %s", e.getMessage())); + continue; + } + commandHandler.handleCommand(cmd); + } + + ui.farewell(); + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/command/Command.java b/src/main/java/cn/yfshadaow/cs2113/ip/command/Command.java new file mode 100644 index 000000000..d70cfebd4 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/command/Command.java @@ -0,0 +1,53 @@ +package cn.yfshadaow.cs2113.ip.command; + +import java.util.*; + +/** + * Represents a command. A command must have a name, but might have empty arguments and extra arguments. + */ +public class Command { + private String name; + + /** + * Gets name. + * + * @return the name + */ + @SuppressWarnings("unused") + public String getName() { + return name; + } + + /** + * Sets name. + * + * @param name the name + */ + @SuppressWarnings("unused") + public void setName(String name) { + this.name = name; + } + + /** + * The Args. + */ + public final List args = new ArrayList<>(); + /** + * The Extra args. + */ + public final Map extraArgs = new HashMap<>(); + + /** + * Instantiates a new Command. + * + * @param name the name + * @param args the args + * @param extraArgs the extra args + */ + public Command(String name, List args, Map extraArgs) { + this.name = name; + this.args.addAll(args); + this.extraArgs.putAll(extraArgs); + } + +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/command/CommandHandler.java b/src/main/java/cn/yfshadaow/cs2113/ip/command/CommandHandler.java new file mode 100644 index 000000000..9e8cefc84 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/command/CommandHandler.java @@ -0,0 +1,221 @@ +package cn.yfshadaow.cs2113.ip.command; + +import cn.yfshadaow.cs2113.ip.XiaoAiBot; +import cn.yfshadaow.cs2113.ip.task.Deadline; +import cn.yfshadaow.cs2113.ip.task.Event; +import cn.yfshadaow.cs2113.ip.task.Task; +import cn.yfshadaow.cs2113.ip.task.Todo; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a command handler. + */ +public class CommandHandler { + + private final XiaoAiBot bot; + + /** + * Instantiates a new Command handler. + * + * @param bot the bot + */ + public CommandHandler(XiaoAiBot bot) { + this.bot = bot; + } + + /** + * Handle command. + * + * @param cmd the command + */ + public void handleCommand(Command cmd) { + bot.getUi().sendSplit(); + switch (cmd.getName()) { + case "bye": { + bot.setShouldQuit(true); + break; + } + case "todo": { + Todo todo; + try { + todo = Todo.parseTodo(cmd); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error parsing task: %s", e.getMessage())); + break; + } + bot.getTaskList().tasks.add(todo); + bot.getUi().sendMessageWithoutSplit("Got it. I've added this task:"); + bot.getUi().sendMessageWithoutSplit(todo.toStringWithIsDone()); + bot.getUi().sendMessage("Now you have " + bot.getTaskList().tasks.size() + " tasks in the list."); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "deadline": { + Deadline deadline; + try { + deadline = Deadline.parseDeadline(cmd); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error parsing task: %s", e.getMessage())); + break; + } + bot.getTaskList().tasks.add(deadline); + bot.getUi().sendMessageWithoutSplit("Got it. I've added this task:"); + bot.getUi().sendMessageWithoutSplit(deadline.toStringWithIsDone()); + bot.getUi().sendMessage("Now you have " + bot.getTaskList().tasks.size() + " tasks in the list."); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "event": { + Event event; + try { + event = Event.parseEvent(cmd); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error parsing task: %s", e.getMessage())); + break; + } + bot.getTaskList().tasks.add(event); + bot.getUi().sendMessageWithoutSplit("Got it. I've added this task:"); + bot.getUi().sendMessageWithoutSplit(event.toStringWithIsDone()); + bot.getUi().sendMessage("Now you have " + bot.getTaskList().tasks.size() + " tasks in the list."); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "mark": { + if (cmd.args.size() != 1) { + bot.getUi().sendMessage("Incorrect arguments"); + break; + } + int index; + try { + index = Integer.parseInt(cmd.args.get(0)); + } catch (NumberFormatException e) { + bot.getUi().sendMessage(String.format("Error parsing int: %s", e.getMessage())); + break; + } + try { + bot.getTaskList().tasks.get(index - 1).setDone(true); + } catch (IndexOutOfBoundsException e) { + bot.getUi().sendMessage(String.format("Error getting task: %s", e.getMessage())); + break; + } + bot.getUi().sendMessageWithoutSplit("Nice! I've marked this task as done:"); + bot.getUi().sendMessage(bot.getTaskList().tasks.get(index - 1).toStringWithIsDone()); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "unmark": { + if (cmd.args.size() != 1) { + bot.getUi().sendMessage("Incorrect arguments"); + break; + } + int index; + try { + index = Integer.parseInt(cmd.args.get(0)); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error parsing int: %s", e.getMessage())); + break; + } + try { + bot.getTaskList().tasks.get(index - 1).setDone(false); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error getting task: %s", e.getMessage())); + break; + } + bot.getUi().sendMessageWithoutSplit("OK, I've marked this task as not done yet:"); + bot.getUi().sendMessage(bot.getTaskList().tasks.get(index - 1).toStringWithIsDone()); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "delete": { + if (cmd.args.size() != 1) { + bot.getUi().sendMessage("Incorrect arguments"); + break; + } + int index; + try { + index = Integer.parseInt(cmd.args.get(0)); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error parsing int: %s", e.getMessage())); + break; + } + Task task; + try { + task = bot.getTaskList().tasks.get(index - 1); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error getting task: %s", e.getMessage())); + break; + } + bot.getUi().sendMessageWithoutSplit("Noted. I've removed this task:"); + bot.getUi().sendMessage(task.toStringWithIsDone()); + bot.getTaskList().tasks.remove(index - 1); + try { + bot.getStorage().saveData(bot.getTaskList()); + } catch (Exception e) { + bot.getUi().sendMessage(String.format("Error saving data: %s", e.getMessage())); + break; + } + break; + } + case "list": { + List tasks = bot.getTaskList().tasks; + for (int i = 0; i < tasks.size(); i += 1) { + Task task = tasks.get(i); + bot.getUi().sendMessageWithoutSplit((i + 1) + "." + task.toStringWithIsDone()); + } + bot.getUi().sendSplit(); + break; + } + case "find" : { + if (cmd.args.isEmpty()) { + bot.getUi().sendMessage("Please enter keyword for searching"); + break; + } + String searchKeyWord = String.join(" ", cmd.args); + if (searchKeyWord.isEmpty()) { + bot.getUi().sendMessage("Search keyword cannot be empty"); + break; + } + bot.getUi().sendMessageWithoutSplit("Here are the matching tasks in your list:"); + List tasks = bot.getTaskList().tasks; + for (int i = 0; i < tasks.size(); i += 1) { + Task task = tasks.get(i); + if (task.getName().toLowerCase().contains(searchKeyWord.toLowerCase())) { + bot.getUi().sendMessageWithoutSplit((i + 1) + "." + task.toStringWithIsDone()); + } + } + bot.getUi().sendSplit(); + break; + } + default: { + bot.getUi().sendMessage("Unknown command"); + } + } + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/storage/Storage.java b/src/main/java/cn/yfshadaow/cs2113/ip/storage/Storage.java new file mode 100644 index 000000000..9bb47df5f --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/storage/Storage.java @@ -0,0 +1,91 @@ +package cn.yfshadaow.cs2113.ip.storage; + +import cn.yfshadaow.cs2113.ip.XiaoAiBot; +import cn.yfshadaow.cs2113.ip.task.Deadline; +import cn.yfshadaow.cs2113.ip.task.Event; +import cn.yfshadaow.cs2113.ip.task.Task; +import cn.yfshadaow.cs2113.ip.task.Todo; +import cn.yfshadaow.cs2113.ip.utils.TaskList; +import com.google.gson.*; +import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Represents a storage class used for saving and loading data. + */ +public class Storage { + /** + * Initialize the storage instance by creating necessary files and directories. + * + * @throws IOException the io exception + */ + public void initialize() throws IOException { + Files.createDirectories(Paths.get(DATA_DIRECTORY_PATH)); + taskDataFile.createNewFile(); + } + + private static final String DATA_DIRECTORY_PATH = "./data/"; + + private static final String TASK_DATA_FILE_PATH = DATA_DIRECTORY_PATH + "taskData.json"; + + private final File taskDataFile = new File(TASK_DATA_FILE_PATH); + + private static final RuntimeTypeAdapterFactory taskAdapterFactory = RuntimeTypeAdapterFactory.of(Task.class, "type") + .registerSubtype(Deadline.class, "Deadline") + .registerSubtype(Event.class, "Event") + .registerSubtype(Todo.class, "Todo"); + /** + * The constant gson. + */ + public static final Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(taskAdapterFactory) + .setPrettyPrinting() + .create(); + + /** + * Save data. + * + * @param list the list + * @throws IOException the io exception + */ + public void saveData(TaskList list) throws IOException { + JsonObject dataObject = new JsonObject(); + JsonArray tasksArray = new JsonArray(); + for (Task t: list.tasks) { + if (t instanceof Todo) { + tasksArray.add(gson.toJsonTree(t, Task.class)); + } else if (t instanceof Deadline) { + tasksArray.add(gson.toJsonTree(t, Task.class)); + } else if (t instanceof Event) { + tasksArray.add(gson.toJsonTree(t, Task.class)); + } + } + dataObject.add("tasks", tasksArray); + FileUtils.write(taskDataFile, gson.toJson(dataObject), StandardCharsets.UTF_8); + } + + /** + * Load data task list. + * + * @return the task list + * @throws IOException the io exception + * @throws IllegalStateException the illegal state exception + * @throws JsonParseException the json parse exception + */ + public TaskList loadData() throws IOException, IllegalStateException, JsonParseException { + TaskList list = new TaskList(); + String dataString = FileUtils.readFileToString(taskDataFile, StandardCharsets.UTF_8); + JsonObject dataObject = JsonParser.parseString(dataString).getAsJsonObject(); + JsonArray tasksArray = dataObject.getAsJsonArray("tasks"); + for (JsonElement e : tasksArray.asList()) { + list.tasks.add(gson.fromJson(e, Task.class)); + } + return list; + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/task/Deadline.java b/src/main/java/cn/yfshadaow/cs2113/ip/task/Deadline.java new file mode 100644 index 000000000..db27207f5 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/task/Deadline.java @@ -0,0 +1,68 @@ +package cn.yfshadaow.cs2113.ip.task; + +import cn.yfshadaow.cs2113.ip.command.Command; + +/** + * Represents a deadline task. + */ +public class Deadline extends Task { + private String by; + + /** + * Gets by. + * + * @return the by + */ + @SuppressWarnings("unused") + public String getBy() { + return by; + } + + /** + * Sets by. + * + * @param by the by + */ + @SuppressWarnings("unused") + public void setBy(String by) { + this.by = by; + } + + + /** + * Instantiates a new Deadline. + * + * @param name task name + * @param by the by attribute + */ + public Deadline(String name, String by) { + this.name = name; + this.by = by; + } + + @Override + public String toStringWithIsDone() { + return "[D][" + (isDone ? "X" : " ") + "] " + name + " (by: " + by + ")"; + } + + /** + * Parse deadline from command. + * + * @param cmd the cmd + * @return the deadline + * @throws IllegalArgumentException the illegal argument exception + */ + public static Deadline parseDeadline(Command cmd) throws IllegalArgumentException{ + if (cmd.args.isEmpty()) { + throw new IllegalArgumentException("Deadline name cannot be empty!"); + } + if (!cmd.extraArgs.containsKey("by")) { + throw new IllegalArgumentException("Deadline must have -by argument!"); + } + String byString = cmd.extraArgs.get("by"); + if (byString.isEmpty()) { + throw new IllegalArgumentException("-by argument cannot be empty!"); + } + return new Deadline(String.join(" ", cmd.args), byString); + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/task/Event.java b/src/main/java/cn/yfshadaow/cs2113/ip/task/Event.java new file mode 100644 index 000000000..090c65564 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/task/Event.java @@ -0,0 +1,98 @@ +package cn.yfshadaow.cs2113.ip.task; + +import cn.yfshadaow.cs2113.ip.command.Command; + +/** + * Represents an event task. + */ +public class Event extends Task { + + private String from; + private String to; + + /** + * Gets from. + * + * @return the from + */ + @SuppressWarnings("unused") + public String getFrom() { + return from; + } + + /** + * Sets from. + * + * @param from the from + */ + @SuppressWarnings("unused") + public void setFrom(String from) { + this.from = from; + } + + /** + * Gets to. + * + * @return the to + */ + @SuppressWarnings("unused") + public String getTo() { + return to; + } + + /** + * Sets to. + * + * @param to the to + */ + @SuppressWarnings("unused") + public void setTo(String to) { + this.to = to; + } + + /** + * Instantiates a new Event. + * + * @param name the name of task + * @param from the from attribute + * @param to the to attribute + */ + public Event(String name, String from, String to) { + this.name = name; + this.from = from; + this.to = to; + } + + @Override + public String toStringWithIsDone() { + return "[E][" + (isDone ? "X" : " ") + "] " + name + " (from: " + from + " to: " + to + ")"; + } + + /** + * Parse event from command. + * + * @param cmd the cmd + * @return the event + * @throws IllegalArgumentException the illegal argument exception + */ + public static Event parseEvent(Command cmd) throws IllegalArgumentException{ + if (cmd.args.isEmpty()) { + throw new IllegalArgumentException("Event name cannot be empty!"); + } + if (!cmd.extraArgs.containsKey("from")) { + throw new IllegalArgumentException("Event must have -from argument!"); + } + String fromString = cmd.extraArgs.get("from"); + if (fromString.isEmpty()) { + throw new IllegalArgumentException("-from argument cannot be empty!"); + } + if (!cmd.extraArgs.containsKey("to")) { + throw new IllegalArgumentException("Event must have -to argument!"); + } + String toString = cmd.extraArgs.get("to"); + if (toString.isEmpty()) { + throw new IllegalArgumentException("-to argument cannot be empty!"); + } + return new Event(String.join(" ", cmd.args), fromString, toString); + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/task/Task.java b/src/main/java/cn/yfshadaow/cs2113/ip/task/Task.java new file mode 100644 index 000000000..a9f57065e --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/task/Task.java @@ -0,0 +1,67 @@ +package cn.yfshadaow.cs2113.ip.task; + +import com.google.gson.annotations.Expose; + +/** + * Represents an abstract task. + */ +public abstract class Task { + /** + * Whether this task is done. + */ + @Expose + protected boolean isDone = false; + /** + * The task name. + */ + @Expose + protected String name; + + /** + * Gets whether the task is done. + * + * @return whether task is done + */ + @SuppressWarnings("unused") + public boolean isDone() { + return isDone; + } + + /** + * Sets whether the task is done + * + * @param done the boolean value to be set + */ + public void setDone(boolean done) { + isDone = done; + } + + /** + * Gets name. + * + * @return the name of task + */ + @SuppressWarnings("unused") + public String getName() { + return name; + } + + /** + * Sets name. + * + * @param name the name of task + */ + @SuppressWarnings("unused") + public void setName(String name) { + this.name = name; + } + + /** + * Gets a string used for printing by UI + * + * @return the formatted string + */ + public String toStringWithIsDone() { + return "[" + (isDone ? "X" : " ") + "] " + name; + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/task/Todo.java b/src/main/java/cn/yfshadaow/cs2113/ip/task/Todo.java new file mode 100644 index 000000000..c50ca68b1 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/task/Todo.java @@ -0,0 +1,36 @@ +package cn.yfshadaow.cs2113.ip.task; + +import cn.yfshadaow.cs2113.ip.command.Command; + +/** + * Represents a todo task. + */ +public class Todo extends Task { + /** + * Instantiates a new Todo. + * + * @param name the name of task + */ + public Todo(String name) { + this.name = name; + } + + /** + * Parse todo from command + * + * @param cmd the command + * @return the todo task + * @throws IllegalArgumentException the illegal argument exception + */ + public static Todo parseTodo(Command cmd) throws IllegalArgumentException{ + if (cmd.args.isEmpty()) { + throw new IllegalArgumentException("Todo name cannot be empty!"); + } + return new Todo(String.join(" ", cmd.args)); + } + + @Override + public String toStringWithIsDone() { + return "[T][" + (isDone ? "X" : " ") + "] " + name; + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/ui/Ui.java b/src/main/java/cn/yfshadaow/cs2113/ip/ui/Ui.java new file mode 100644 index 000000000..59a390e5a --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/ui/Ui.java @@ -0,0 +1,82 @@ +package cn.yfshadaow.cs2113.ip.ui; + +import cn.yfshadaow.cs2113.ip.XiaoAiBot; + +import java.util.Scanner; + +/** + * The UI class used for receiving user's input and printing output + */ +public class Ui { + private static final String BOT_NAME = "XiaoAi"; + private static final String SPLIT = "____________________________________________"; + private static final String GREET_MESSAGE = "Welcome back, master!\n" + + BOT_NAME + " here. What can I do for you?"; + + private static final String QUIT_MESSAGE = "See you next time, master!"; + private final XiaoAiBot bot; + private final Scanner scanner = new Scanner(System.in); + + /** + * Instantiates a new Ui instance. + * + * @param bot the bot + */ + public Ui(XiaoAiBot bot) { + this.bot = bot; + } + + /** + * Send message to user. + * + * @param message the message to be sent + */ + public void sendMessage(String message) { + sendMessageWithoutSplit(message); + sendSplit(); + } + + /** + * Greets the user. + */ + public void greet() { + sendMessage(GREET_MESSAGE); + } + + /** + * Farewell to the user. + */ + public void farewell() { + sendMessage(QUIT_MESSAGE); + } + + /** + * Send message without split. + * + * @param message the message to be sent + */ + public void sendMessageWithoutSplit(String message) { + System.out.println(message); + } + + /** + * Send split line to user + */ + public void sendSplit() { + System.out.println(SPLIT); + } + + /** + * Read line from scanner + * + * @return the string read from the scanner + */ + public String readLine() { + try { + return scanner.nextLine(); + } catch (Exception e) { + sendMessage(String.format("Error reading line: %s", e.getMessage())); + return null; + } + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/utils/Parser.java b/src/main/java/cn/yfshadaow/cs2113/ip/utils/Parser.java new file mode 100644 index 000000000..e45f49113 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/utils/Parser.java @@ -0,0 +1,51 @@ +package cn.yfshadaow.cs2113.ip.utils; + +import cn.yfshadaow.cs2113.ip.command.Command; + +import java.util.*; + +/** + * Represents a parser to parse commands. + */ +public class Parser { + + /** + * Parse command from string. + * + * @param s the string to be parsed + * @return the command parsed + * @throws IllegalArgumentException the illegal argument exception + */ + public static Command parseCommand(String s) throws IllegalArgumentException{ + Iterator iterator = Arrays.stream(s.split(" ")).iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException("Command cannot be empty!"); + } + String cmdString = iterator.next(); + List arguments = new ArrayList<>(); + Map extraArguments = new HashMap<>(); + + String currentTargetKey = null; + while (iterator.hasNext()) { + String next = iterator.next(); + if (next.startsWith("-")) { + if (next.length() == 1) { + throw new IllegalArgumentException("Extra argument identifier cannot be empty!"); + } + currentTargetKey = next.substring(1); + if (extraArguments.containsKey(currentTargetKey)) { + throw new IllegalArgumentException("Duplicate argument identifier!"); + } + extraArguments.put(currentTargetKey, ""); + } else { + if (currentTargetKey == null) { + arguments.add(next); + } else { + String storedString = extraArguments.get(currentTargetKey); + extraArguments.put(currentTargetKey, storedString + (storedString.isEmpty()? "" : " ") + next); + } + } + } + return new Command(cmdString, arguments, extraArguments); + } +} diff --git a/src/main/java/cn/yfshadaow/cs2113/ip/utils/TaskList.java b/src/main/java/cn/yfshadaow/cs2113/ip/utils/TaskList.java new file mode 100644 index 000000000..1949bf8b4 --- /dev/null +++ b/src/main/java/cn/yfshadaow/cs2113/ip/utils/TaskList.java @@ -0,0 +1,18 @@ +package cn.yfshadaow.cs2113.ip.utils; + +import cn.yfshadaow.cs2113.ip.XiaoAiBot; +import cn.yfshadaow.cs2113.ip.task.Task; + +import java.util.LinkedList; +import java.util.List; + +/** + * Represents a task list. + */ +public class TaskList { + + /** + * The Tasks. + */ + public final List tasks = new LinkedList<>(); +} diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java new file mode 100644 index 000000000..29fbc6926 --- /dev/null +++ b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.typeadapters; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap<>(); + private final Map, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + private boolean recognizeSubtypes; + + private RuntimeTypeAdapterFactory( + Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * + * @param maintainType true if the type field should be included in deserialized objects + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /** + * Ensures that this factory will handle not just the given {@code baseType}, but any subtype + * of that type. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory recognizeSubtypes() { + this.recognizeSubtypes = true; + return this; + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + @CanIgnoreReturnValue + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + Class rawType = type.getRawType(); + boolean handle = + recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); + if (!handle) { + return null; + } + + final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + final Map> labelToDelegate = new LinkedHashMap<>(); + final Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = jsonElementAdapter.read(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } +} diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 087374464..5fcdaa0c2 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -7,7 +7,7 @@ REM delete output from previous run if exist ACTUAL.TXT del ACTUAL.TXT REM compile the code into the bin folder -javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\*.java +javac -cp ..\src\main\java -Xlint:none -d ..\bin ..\src\main\java\cn\yfshadaow\cs2113\ip\*.java IF ERRORLEVEL 1 ( echo ********** BUILD FAILURE ********** exit /b 1 @@ -15,7 +15,7 @@ IF ERRORLEVEL 1 ( REM no error here, errorlevel == 0 REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ..\bin Duke < input.txt > ACTUAL.TXT +java -classpath ..\bin cn.yfshadaow.cs2113.ip.Duke < input.txt > ACTUAL.TXT REM compare the output to the expected output FC ACTUAL.TXT EXPECTED.TXT diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index c9ec87003..d6a236db2 100644 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -13,14 +13,14 @@ then fi # compile the code into the bin folder, terminates if error occurred -if ! javac -cp ../src/main/java -Xlint:none -d ../bin ../src/main/java/*.java +if ! javac -cp ../src/main/java -Xlint:none -d ../bin ../src/main/java/cn/yfshadaow/cs2113/ip/*.java then echo "********** BUILD FAILURE **********" exit 1 fi # run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT -java -classpath ../bin Duke < input.txt > ACTUAL.TXT +java -classpath ../bin cn.yfshadaow.cs2113.ip.Duke < input.txt > ACTUAL.TXT # convert to UNIX format cp EXPECTED.TXT EXPECTED-UNIX.TXT