diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
new file mode 100644
index 00000000..57f68586
--- /dev/null
+++ b/.github/workflows/workflow.yml
@@ -0,0 +1,45 @@
+on: push
+name: Build, Test and Upload JAR
+jobs:
+ checks:
+ name: Run Unit Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+
+ - name: Mount bazel cache
+ uses: actions/cache@v1
+ with:
+ path: "/home/runner/.cache/bazel"
+ key: bazel
+
+ - name: Install bazelisk
+ run: |
+ curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64"
+ mkdir -p "${GITHUB_WORKSPACE}/bin/"
+ mv bazelisk-linux-amd64 "${GITHUB_WORKSPACE}/bin/bazel"
+ chmod +x "${GITHUB_WORKSPACE}/bin/bazel"
+
+ - name: Run Unit Tests
+ run: |
+ "${GITHUB_WORKSPACE}/bin/bazel" test //...
+
+ - name: Run UI and Serialization Tests
+ working-directory: ./text-ui-test
+ run: |
+ chmod +x runtest.sh
+ ./runtest.sh
+
+ - name: Build JAR File
+ run: |
+ "${GITHUB_WORKSPACE}/bin/bazel" build //:TerminalDuke_deploy.jar
+
+ - name: "Move file to non-symlink directory (a workaround for some bug See: https://github.com/actions/upload-artifact/issues/92)"
+ run: |
+ mv ${{ github.workspace }}/bazel-bin/TerminalDuke_deploy.jar ${{ github.workspace }}/TerminalDuke_deploy.jar
+
+ - name: Upload JAR File
+ uses: actions/upload-artifact@v2
+ with:
+ name: TerminalDuke_deploy.jar
+ path: ${{ github.workspace }}/TerminalDuke_deploy.jar
diff --git a/.gitignore b/.gitignore
index f69985ef..d08823f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,12 @@ src/main/resources/docs/
bin/
/text-ui-test/ACTUAL.txt
+/text-ui-test/tasks.json
text-ui-test/EXPECTED-UNIX.TXT
+
+# Ignore all bazel-* symlinks. There is no full list since this can change
+# based on the name of the directory bazel is cloned into.
+/bazel-*
+
+# for pre-commit git-hooks
+/.cache/*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..f4b6f43b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,7 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/maltzj/google-style-precommit-hook
+ rev: b7e9e7fcba4a5aea463e72fe9964c14877bd8130
+ hooks:
+ - id: google-style-java
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 00000000..1641d4ff
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,8 @@
+# Pre-commit hook to run google-format-java right before commiting
+# Source: https://github.com/maltzj/google-style-precommit-hook
+- id: google-style-java
+ name: Google Java Code Style for Java
+ description: Formats code in Google's Java codestyle.
+ entry: ./tools/format-code.sh
+ language: script
+ files: \.java$ # We don't technically need this, as the script will filter for us, but this will allow the hook to be skipped if no Java is changed.
diff --git a/BUILD b/BUILD
new file mode 100644
index 00000000..ee30cb50
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,54 @@
+load("@rules_java//java:defs.bzl", "java_binary", "java_test")
+
+java_binary(
+ name = "TerminalDuke", # Production Build.
+ srcs = glob([
+ "src/main/java/com/lockarhythm/**/*.java",
+ ]),
+ deps = [
+ "@maven//:org_apache_commons_commons_lang3",
+ "@maven//:com_google_code_gson_gson",
+ "@maven//:org_ocpsoft_prettytime_prettytime_nlp",
+ ],
+)
+
+java_binary(
+ name = "TerminalDukeQA", # QA Build. has assertions enabled.
+ main_class = "com.lockarhythm.application.TerminalDuke",
+ srcs = glob([
+ "src/main/java/com/lockarhythm/**/*.java",
+ ]),
+ deps = [
+ "@maven//:org_apache_commons_commons_lang3",
+ "@maven//:com_google_code_gson_gson",
+ "@maven//:org_ocpsoft_prettytime_prettytime_nlp",
+ ],
+ jvm_flags = [
+ "-enableassertions",
+ ],
+)
+
+[
+ java_test(
+ name = class_name,
+ size = "small",
+ srcs = glob([
+ "src/test/java/com/lockarhythm/**/*.java",
+ "src/main/java/com/lockarhythm/**/*.java",
+ ]),
+ deps = [
+ "@maven//:org_apache_commons_commons_lang3",
+ "@maven//:com_google_code_gson_gson",
+ "@maven//:org_ocpsoft_prettytime_prettytime_nlp",
+ ]
+ )
+ for class_name in [
+ "TestTerminalDuke",
+ "TestEchoResponder",
+ "TestExitResponder",
+ "TestListResponder",
+ "TestMarkAsDoneResponder",
+ "TestTodoTask",
+ "TestTaskList",
+ ]
+]
diff --git a/README.md b/README.md
index 8715d4d9..0dfacebc 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,61 @@
-# Duke project template
+
+
+# Duke
This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it.
-## Setting up in Intellij
-
-Prerequisites: JDK 11, update Intellij to the most recent version.
-
-1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first)
-1. Open the project into Intellij as follows:
- 1. Click `Open`.
- 1. Select the project directory, and click `OK`.
- 1. If there are any further prompts, accept the defaults.
-1. Configure the project to use **JDK 11** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
- In the same dialog, set the **Project language level** field to the `SDK default` option.
-3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output:
- ```
- Hello from
- ____ _
- | _ \ _ _| | _____
- | | | | | | | |/ / _ \
- | |_| | |_| | < __/
- |____/ \__,_|_|\_\___|
- ```
+## Demo
+
+
+
+## Build
+Prerequisites: Have `bazel` installed.
+
+To build for local development on macOS:
+```bash
+# bazel makes a wrapper script around the jar file.
+bazel build //:TerminalDuke && ./bazel-bin/TerminalDuke
+
+# or simply
+bazel run //:TerminalDuke
+
+# Use bazelisk for M1 Macs (Apple Silicon)
+USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk run //:TerminalDuke
+
+# For QA build, for example, having assertions enabled, build TerminalDukeQA instead
+USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk run //:TerminalDukeQA
+```
+
+To build jar file for deployment purposes:
+```bash
+bazel build //:Duke_deploy.jar
+
+java -jar ./bazel-bin/Duke_deploy.jar
+```
+
+## Testing
+For unit tests:
+```bash
+bazel test --test_output=all //...
+```
+For terminal ui tests (`text-ui-test`):
+```bash
+cd text-ui-test
+./runtest.sh
+```
+
+## Code Formatting & Style
+Use [google-java-format](https://github.com/google/google-java-format):
+```bash
+google-java-format --replace **/*.java # executes recursively
+```
+
+## Pre-commit Git Hook (Tested on macOS)
+`pre-commit` is a python tool that helps to create git pre-commit hook handlers. For this project, it is used to enforce Google Java Style (via `google-java-format`) on new commits.
+
+To setup, ensure you have [`pre-commit`](https://pre-commit.com/#install) installed, then run:
+```bash
+pre-commit install
+```
+This only needs to be executed once, so that `pre-commit` can generate the script and write it to `.git/hooks/pre-commit`
+Thereafter, it should be executed right before every commit!
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 00000000..7c359742
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,27 @@
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+RULES_JVM_EXTERNAL_TAG = "4.0"
+RULES_JVM_EXTERNAL_SHA = "31701ad93dbfe544d597dbe62c9a1fdd76d81d8a9150c2bf1ecf928ecdf97169"
+
+http_archive(
+ name = "rules_jvm_external",
+ strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
+ sha256 = RULES_JVM_EXTERNAL_SHA,
+ url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
+)
+
+load("@rules_jvm_external//:defs.bzl", "maven_install")
+
+maven_install(
+ artifacts = [
+ #"com.google.guava:guava:21.0",
+ "org.apache.commons:commons-lang3:3.11",
+ "com.google.code.gson:gson:2.8.8",
+ "org.ocpsoft.prettytime:prettytime-nlp:5.0.2.Final"
+ ],
+ repositories = [
+ # Private repositories are supported through HTTP Basic auth
+ "https://maven.google.com",
+ "https://repo1.maven.org/maven2",
+ ],
+)
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 00000000..4c001417
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,403 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 00000000..39efb6e4
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java
deleted file mode 100644
index 5d313334..00000000
--- 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/com/lockarhythm/application/Application.java b/src/main/java/com/lockarhythm/application/Application.java
new file mode 100644
index 00000000..5ca8f930
--- /dev/null
+++ b/src/main/java/com/lockarhythm/application/Application.java
@@ -0,0 +1,52 @@
+package com.lockarhythm.application;
+
+import com.lockarhythm.query.DukeException;
+import com.lockarhythm.query.QueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.storage.Storage;
+import com.lockarhythm.ui.UI;
+import java.io.IOException;
+
+/**
+ * Application contains the common logic of all Duke Applications (GUI, TUI).
+ *
+ *
The application architecture is heavily inspired by the Ports & Adapters architecture.
+ */
+abstract class Application {
+ static String logo =
+ " \t____ _ \n"
+ + "\t| _ \\ _ _| | _____ \n"
+ + "\t| | | | | | | |/ / _ \\\n"
+ + "\t| |_| | |_| | < __/\n"
+ + "\t|____/ \\__,_|_|\\_\\___|\n";
+
+ /**
+ * run should be supplied with concrete implementations of its UI, QueryInterpreter and Storage.
+ *
+ *
For example, UI could be the Terminal UI or GUI. Storage could be in-memory or disk.
+ *
+ * @param ui a concrete UI implementation
+ * @param q a concrete QueryInterpreter implementation
+ * @param storage any Storage implementation
+ */
+ public static void run(UI ui, QueryInterpreter q, Storage storage) {
+ Result result;
+
+ ui.print("Hello I'm\n" + logo, "What can I do for you?");
+
+ while (ui.hasNext()) {
+ try {
+ result = q.interpret(ui.nextLine());
+ storage.overwrite();
+ ui.print(result);
+ if (result.shouldExit()) {
+ break;
+ }
+ } catch (IOException e) {
+ ui.print(String.format("Sorry, I cannot save the task to file: %s", e));
+ } catch (DukeException e) {
+ ui.print("Sorry, I don't understand that yet!");
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/lockarhythm/application/TerminalDuke.java b/src/main/java/com/lockarhythm/application/TerminalDuke.java
new file mode 100644
index 00000000..d961ba6e
--- /dev/null
+++ b/src/main/java/com/lockarhythm/application/TerminalDuke.java
@@ -0,0 +1,23 @@
+package com.lockarhythm.application;
+
+import com.lockarhythm.query.SimpleQueryInterpreter;
+import com.lockarhythm.storage.Storage;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+import com.lockarhythm.ui.TerminalUI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+
+/**
+ * TerminalDuke is the entrypoint of the Terminal UI application of Duke.
+ */
+final class TerminalDuke extends Application {
+ public static void main(String[] args) {
+ Path path = Paths.get(".", "tasks.json");
+ Storage storage = new Storage(path.toString());
+ ArrayList list = storage.load(Task.class);
+ storage.registerList(list);
+ run(new TerminalUI(), new SimpleQueryInterpreter(new TaskList(list)), storage);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/AddedTaskResult.java b/src/main/java/com/lockarhythm/query/AddedTaskResult.java
new file mode 100644
index 00000000..fe81905a
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/AddedTaskResult.java
@@ -0,0 +1,20 @@
+package com.lockarhythm.query;
+
+import com.lockarhythm.tasks.Task;
+
+public class AddedTaskResult extends Result {
+ private Task task;
+ private int size; // the max size on add
+
+ public AddedTaskResult(Task task, int size) {
+ super(task.toString());
+ this.task = task;
+ this.size = size;
+ }
+
+ @Override
+ public String getText() {
+ return String.format(
+ "Got it. I've added this task:\n\t%s\nNow you have %d tasks in the list.", task, size);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/DeletedTaskResult.java b/src/main/java/com/lockarhythm/query/DeletedTaskResult.java
new file mode 100644
index 00000000..128ac60a
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/DeletedTaskResult.java
@@ -0,0 +1,20 @@
+package com.lockarhythm.query;
+
+import com.lockarhythm.tasks.Task;
+
+public class DeletedTaskResult extends Result {
+ private Task task;
+ private int size; // new size after deletion
+
+ public DeletedTaskResult(Task task, int size) {
+ super(task.toString());
+ this.task = task;
+ this.size = size;
+ }
+
+ @Override
+ public String getText() {
+ return String.format(
+ "Noted. I've removed this task:\n\t%s\nNow you have %d tasks in the list.", task, size);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/DukeException.java b/src/main/java/com/lockarhythm/query/DukeException.java
new file mode 100644
index 00000000..42f0711f
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/DukeException.java
@@ -0,0 +1,3 @@
+package com.lockarhythm.query;
+
+public class DukeException extends Exception {}
diff --git a/src/main/java/com/lockarhythm/query/QueryInterpreter.java b/src/main/java/com/lockarhythm/query/QueryInterpreter.java
new file mode 100644
index 00000000..9d856d31
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/QueryInterpreter.java
@@ -0,0 +1,5 @@
+package com.lockarhythm.query;
+
+public interface QueryInterpreter {
+ public Result interpret(String query) throws DukeException;
+}
diff --git a/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java b/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java
new file mode 100644
index 00000000..1c19bd8e
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/RegexQueryInterpreter.java
@@ -0,0 +1,34 @@
+package com.lockarhythm.query;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class RegexQueryInterpreter implements QueryInterpreter {
+ private Pattern pattern;
+
+ public RegexQueryInterpreter() {
+ pattern = Pattern.compile(commandRegex());
+ }
+
+ public Result interpret(String query) {
+ Matcher matcher = pattern.matcher(query);
+ if (matcher.find()) {
+ return onMatch(toGroupStrings(matcher));
+ }
+ return null;
+ }
+
+ private String[] toGroupStrings(Matcher matcher) {
+ String[] groups = new String[matcher.groupCount() + 1];
+ for (int i = 0; i <= matcher.groupCount(); i++) {
+ groups[i] = matcher.group(i);
+ }
+ return groups;
+ }
+
+ // onMatch is called when regexp pattern matches. The regexp capturing groups is passed as
+ // arguments.
+ public abstract Result onMatch(String[] groups);
+
+ protected abstract String commandRegex();
+}
diff --git a/src/main/java/com/lockarhythm/query/Result.java b/src/main/java/com/lockarhythm/query/Result.java
new file mode 100644
index 00000000..0a974d05
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/Result.java
@@ -0,0 +1,23 @@
+package com.lockarhythm.query;
+
+public class Result {
+ private String text;
+ private boolean shouldExit;
+
+ public Result(String text, boolean shouldExit) {
+ this.text = text;
+ this.shouldExit = shouldExit;
+ }
+
+ public Result(String text) {
+ this(text, false);
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public boolean shouldExit() {
+ return shouldExit;
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java b/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java
new file mode 100644
index 00000000..b7dc2e39
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/SimpleQueryInterpreter.java
@@ -0,0 +1,42 @@
+package com.lockarhythm.query;
+
+import com.lockarhythm.query.deadline.DeadlineResponder;
+import com.lockarhythm.query.delete.DeleteResponder;
+import com.lockarhythm.query.event.EventResponder;
+import com.lockarhythm.query.exit.ExitResponder;
+import com.lockarhythm.query.find.FindResponder;
+import com.lockarhythm.query.list.ListResponder;
+import com.lockarhythm.query.markasdone.MarkAsDoneResponder;
+import com.lockarhythm.query.sort.SortResponder;
+import com.lockarhythm.query.todo.TodoResponder;
+import com.lockarhythm.tasks.TaskList;
+
+/** SimpleQueryInterpreter finds the first QueryInterpreter that responds a non-null result. */
+public final class SimpleQueryInterpreter implements QueryInterpreter {
+ static QueryInterpreter[] interpreters;
+
+ public SimpleQueryInterpreter(TaskList list) {
+ QueryInterpreter[] res = {
+ new ExitResponder(),
+ new MarkAsDoneResponder(list),
+ new TodoResponder(list),
+ new ListResponder(list),
+ new DeadlineResponder(list),
+ new EventResponder(list),
+ new DeleteResponder(list),
+ new SortResponder(list),
+ new FindResponder(list),
+ };
+ interpreters = res;
+ }
+
+ public Result interpret(String query) throws DukeException {
+ for (QueryInterpreter interpreter : interpreters) {
+ Result res = interpreter.interpret(query);
+ if (res != null) {
+ return res;
+ }
+ }
+ throw new DukeException();
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java b/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java
new file mode 100644
index 00000000..8e6cc1bd
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/deadline/DeadlineResponder.java
@@ -0,0 +1,29 @@
+package com.lockarhythm.query.deadline;
+
+import com.lockarhythm.query.AddedTaskResult;
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+
+public class DeadlineResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public DeadlineResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^deadline (.+) \\/by (.+)";
+ }
+
+ public Result onMatch(String[] groups) {
+ try {
+ Task task = list.addDeadlineTask(groups[1], groups[2]);
+ return new AddedTaskResult(task, list.size());
+ } catch (java.time.format.DateTimeParseException e) {
+ return new Result(
+ String.format("Please give me a valid LocalDate pattern: %s", e.getMessage()));
+ }
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java b/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java
new file mode 100644
index 00000000..7dfa9b10
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/delete/DeleteResponder.java
@@ -0,0 +1,29 @@
+package com.lockarhythm.query.delete;
+
+import com.lockarhythm.query.DeletedTaskResult;
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+
+public class DeleteResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public DeleteResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^delete (\\d+)";
+ }
+
+ public Result onMatch(String[] groups) {
+ int index = Integer.parseInt(groups[1]);
+ try {
+ Task task = list.deleteTask(index - 1);
+ return new DeletedTaskResult(task, list.size());
+ } catch (IndexOutOfBoundsException e) {
+ return new Result(String.format("Item %d is not on the list. I cannot delete it!", index));
+ }
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/echo/EchoResponder.java b/src/main/java/com/lockarhythm/query/echo/EchoResponder.java
new file mode 100644
index 00000000..7b6f8a32
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/echo/EchoResponder.java
@@ -0,0 +1,10 @@
+package com.lockarhythm.query.echo;
+
+import com.lockarhythm.query.QueryInterpreter;
+import com.lockarhythm.query.Result;
+
+public class EchoResponder implements QueryInterpreter {
+ public Result interpret(String query) {
+ return new Result(query);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/event/EventResponder.java b/src/main/java/com/lockarhythm/query/event/EventResponder.java
new file mode 100644
index 00000000..2a8a088d
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/event/EventResponder.java
@@ -0,0 +1,29 @@
+package com.lockarhythm.query.event;
+
+import com.lockarhythm.query.AddedTaskResult;
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+
+public class EventResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public EventResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^event (.+) \\/at (.+)";
+ }
+
+ public Result onMatch(String[] groups) {
+ try {
+ Task task = list.addEventTask(groups[1], groups[2]);
+ return new AddedTaskResult(task, list.size());
+ } catch (java.time.format.DateTimeParseException e) {
+ return new Result(
+ String.format("Please give me a valid LocalDate pattern: %s", e.getMessage()));
+ }
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/exit/ExitResponder.java b/src/main/java/com/lockarhythm/query/exit/ExitResponder.java
new file mode 100644
index 00000000..452edcd3
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/exit/ExitResponder.java
@@ -0,0 +1,13 @@
+package com.lockarhythm.query.exit;
+
+import com.lockarhythm.query.QueryInterpreter;
+import com.lockarhythm.query.Result;
+
+public class ExitResponder implements QueryInterpreter {
+ public Result interpret(String query) {
+ if (query.equals("bye")) {
+ return new Result("Bye. Hope to see you again soon!", true);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/find/FindResponder.java b/src/main/java/com/lockarhythm/query/find/FindResponder.java
new file mode 100644
index 00000000..0ef60507
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/find/FindResponder.java
@@ -0,0 +1,31 @@
+package com.lockarhythm.query.find;
+
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.TaskList;
+
+public class FindResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public FindResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^find (.*)$";
+ }
+
+ public Result onMatch(String[] groups) {
+ String query = groups[1];
+
+ TaskList filteredList = list.find(query);
+
+ if (filteredList.size() == 0) {
+ return new Result(
+ String.format("I could not find any tasks matching your query '%s'", query));
+ }
+ return new Result(
+ String.format(
+ "Ok! I have found these tasks for your query '%s'!\n%s", query, filteredList));
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/list/ListResponder.java b/src/main/java/com/lockarhythm/query/list/ListResponder.java
new file mode 100644
index 00000000..ba52a90c
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/list/ListResponder.java
@@ -0,0 +1,20 @@
+package com.lockarhythm.query.list;
+
+import com.lockarhythm.query.QueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.TaskList;
+
+public final class ListResponder implements QueryInterpreter {
+ private TaskList list;
+
+ public ListResponder(TaskList list) {
+ this.list = list;
+ }
+
+ public Result interpret(String query) {
+ if (query.equals("list")) {
+ return new Result("Here are the tasks in your list:\n" + list.toString());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java b/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java
new file mode 100644
index 00000000..bcb629c2
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/markasdone/MarkAsDoneResponder.java
@@ -0,0 +1,28 @@
+package com.lockarhythm.query.markasdone;
+
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+
+public class MarkAsDoneResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public MarkAsDoneResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^done (\\d+)";
+ }
+
+ public Result onMatch(String[] groups) {
+ int i = Integer.parseInt(groups[1]);
+ try {
+ Task task = list.markAsDone(i - 1);
+ return new Result("Nice! I've marked this task as done:\n\t" + task.toString());
+ } catch (IndexOutOfBoundsException e) {
+ return new Result(String.format("Item %d is not on the list. I cannot mark it as done!", i));
+ }
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/sort/SortResponder.java b/src/main/java/com/lockarhythm/query/sort/SortResponder.java
new file mode 100644
index 00000000..52ca5e65
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/sort/SortResponder.java
@@ -0,0 +1,59 @@
+package com.lockarhythm.query.sort;
+
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.TaskList;
+
+public class SortResponder extends RegexQueryInterpreter {
+ private static final boolean IS_ASCENDING_DEFAULT = true;
+
+ private TaskList list;
+
+ public SortResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^sort by (date|done)( desc| asc)?$";
+ }
+
+ public Result onMatch(String[] groups) {
+ String sortKey = groups[1];
+ boolean isAscending = parseIsAscending(groups[2]);
+
+ TaskList sortedList = sortBy(sortKey, isAscending);
+
+ return new Result(
+ String.format(
+ "Ok! I have sorted the tasks by %s in %s order!\n%s",
+ sortKey, isAscending ? "ascending" : "descending", sortedList));
+ }
+
+ private boolean parseIsAscending(String value) {
+ if (value == null) {
+ return IS_ASCENDING_DEFAULT;
+ }
+ String s = value.trim();
+ if (s.equals("desc")) {
+ return false;
+ } else if (s.equals("asc")) {
+ return true;
+ } else {
+ return IS_ASCENDING_DEFAULT;
+ }
+ }
+
+ /** Sorts by the `sortKey` in the order given by `isAscending`. */
+ private TaskList sortBy(String sortKey, boolean isAscending) {
+ switch (sortKey) {
+ case "done":
+ return list.sortByDone(isAscending);
+ case "date":
+ return list.sortByTaskDate(isAscending);
+ default:
+ assert false : "sortKey is not a valid value. Check command regex.";
+ }
+ ;
+ return list.sortByTaskDate(isAscending);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/query/todo/TodoResponder.java b/src/main/java/com/lockarhythm/query/todo/TodoResponder.java
new file mode 100644
index 00000000..9e144e27
--- /dev/null
+++ b/src/main/java/com/lockarhythm/query/todo/TodoResponder.java
@@ -0,0 +1,24 @@
+package com.lockarhythm.query.todo;
+
+import com.lockarhythm.query.AddedTaskResult;
+import com.lockarhythm.query.RegexQueryInterpreter;
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskList;
+
+public class TodoResponder extends RegexQueryInterpreter {
+ private TaskList list;
+
+ public TodoResponder(TaskList list) {
+ this.list = list;
+ }
+
+ protected String commandRegex() {
+ return "^todo (.+)";
+ }
+
+ public Result onMatch(String[] groups) {
+ Task task = list.addTodoTask(groups[1]);
+ return new AddedTaskResult(task, list.size());
+ }
+}
diff --git a/src/main/java/com/lockarhythm/storage/GsonLocalDate.java b/src/main/java/com/lockarhythm/storage/GsonLocalDate.java
new file mode 100644
index 00000000..e2fe70bb
--- /dev/null
+++ b/src/main/java/com/lockarhythm/storage/GsonLocalDate.java
@@ -0,0 +1,23 @@
+package com.lockarhythm.storage;
+
+import com.google.gson.*;
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+public class GsonLocalDate implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public LocalDate deserialize(
+ JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext)
+ throws JsonParseException {
+ String ldtString = jsonElement.getAsString();
+ return LocalDate.parse(ldtString, DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ @Override
+ public JsonElement serialize(
+ LocalDate localDate, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(localDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
+ }
+}
diff --git a/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java b/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java
new file mode 100644
index 00000000..1fea45ef
--- /dev/null
+++ b/src/main/java/com/lockarhythm/storage/GsonLocalDateTime.java
@@ -0,0 +1,24 @@
+package com.lockarhythm.storage;
+
+import com.google.gson.*;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class GsonLocalDateTime
+ implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public LocalDateTime deserialize(
+ JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext)
+ throws JsonParseException {
+ String ldtString = jsonElement.getAsString();
+ return LocalDateTime.parse(ldtString, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ }
+
+ @Override
+ public JsonElement serialize(
+ LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+ }
+}
diff --git a/src/main/java/com/lockarhythm/storage/Storage.java b/src/main/java/com/lockarhythm/storage/Storage.java
new file mode 100644
index 00000000..c770d632
--- /dev/null
+++ b/src/main/java/com/lockarhythm/storage/Storage.java
@@ -0,0 +1,85 @@
+package com.lockarhythm.storage;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import com.lockarhythm.tasks.Task;
+import com.lockarhythm.tasks.TaskDeserializer;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+
+/**
+ * Storage handles all concerns relating to persisting tasks to disk.
+ */
+public class Storage {
+ private String filePath;
+
+ private ArrayList list;
+
+ private Gson gson;
+
+ /**
+ * Storage constructor takes in filePath as the destination path to write bytes to.
+ *
+ * @param filePath a valid location path to persist the tasks to.
+ */
+ public Storage(String filePath) {
+ this.filePath = filePath;
+
+ gson =
+ new GsonBuilder()
+ .registerTypeAdapter(Task.class, new TaskDeserializer("_type"))
+ .registerTypeAdapter(LocalDateTime.class, new GsonLocalDateTime())
+ .registerTypeAdapter(LocalDate.class, new GsonLocalDate())
+ .setPrettyPrinting()
+ .create();
+ }
+
+ /**
+ * registerList is meant to allow top-level Application to pass a reference a the Task List so that Storage can save a reference.
+ *
+ * @param list an array list of tasks to be persisted.
+ */
+ public void registerList(ArrayList list) {
+ this.list = list;
+ }
+
+ /**
+ * load reads the array list of tasks from disk. In order to handle persisting of abstract classes like Task with GSON, we need to pass its class in as parameter.
+ *
+ * @param type to pass the abstract type that is expected.
+ * @return an array list of tasks from disk.
+ */
+ public ArrayList load(Class type) {
+ String content;
+ try {
+ content = Files.readString(Path.of(filePath), StandardCharsets.UTF_8);
+
+ Type typeOfT = TypeToken.getParameterized(ArrayList.class, type).getType();
+ ArrayList deserialized =
+ gson.fromJson(content, new TypeToken>() {}.getType());
+ if (deserialized == null) {
+ return new ArrayList();
+ }
+ return deserialized;
+ } catch (IOException e) {
+ return new ArrayList();
+ }
+ }
+
+ /**
+ * overwrite writes the current array list to disk.
+ */
+ public void overwrite() throws IOException {
+ FileOutputStream fo = new FileOutputStream(filePath);
+ String js = gson.toJson(list);
+ fo.write(js.getBytes());
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/DeadlineTask.java b/src/main/java/com/lockarhythm/tasks/DeadlineTask.java
new file mode 100644
index 00000000..d6239988
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/DeadlineTask.java
@@ -0,0 +1,26 @@
+package com.lockarhythm.tasks;
+
+final class DeadlineTask extends Task {
+ private TaskDate by;
+ private String _type = "DEADLINE";
+
+ public DeadlineTask(String description, String by) {
+ super(description);
+ this.by = new TaskDate(by);
+ }
+
+ @Override
+ public TaskDate getTaskDate() {
+ return by;
+ }
+
+ @Override
+ protected String getTaskTypeIcon() {
+ return "D";
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (by: %s)", super.toString(), by);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/EventTask.java b/src/main/java/com/lockarhythm/tasks/EventTask.java
new file mode 100644
index 00000000..60b8c5b2
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/EventTask.java
@@ -0,0 +1,26 @@
+package com.lockarhythm.tasks;
+
+final class EventTask extends Task {
+ private TaskDate at;
+ private String _type = "EVENT";
+
+ public EventTask(String description, String at) {
+ super(description);
+ this.at = new TaskDate(at);
+ }
+
+ @Override
+ public TaskDate getTaskDate() {
+ return at;
+ }
+
+ @Override
+ protected String getTaskTypeIcon() {
+ return "E";
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (at: %s)", super.toString(), at);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/Task.java b/src/main/java/com/lockarhythm/tasks/Task.java
new file mode 100644
index 00000000..4a739823
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/Task.java
@@ -0,0 +1,47 @@
+package com.lockarhythm.tasks;
+
+import java.io.Serializable;
+
+public abstract class Task implements Serializable {
+ private static transient String DEFAULT_TASK_DATE_VALUE = "3000-12-31T23:59:59";
+ private static transient TaskDate DEFAULT_TASK_DATE = new TaskDate(DEFAULT_TASK_DATE_VALUE);
+
+ private String description;
+ private boolean isDone;
+
+ public Task(String description) {
+ this.description = description;
+ this.isDone = false;
+ }
+
+ public void toggleDone() {
+ this.isDone = !this.isDone;
+ }
+
+ public void setDone(boolean isDone) {
+ this.isDone = isDone;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public boolean isDone() {
+ return isDone;
+ }
+
+ public TaskDate getTaskDate() {
+ return DEFAULT_TASK_DATE;
+ }
+
+ private String getDoneIcon() {
+ return isDone ? "X" : " ";
+ }
+
+ protected abstract String getTaskTypeIcon();
+
+ @Override
+ public String toString() {
+ return String.format("[%s][%s] %s", getTaskTypeIcon(), getDoneIcon(), description);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/TaskDate.java b/src/main/java/com/lockarhythm/tasks/TaskDate.java
new file mode 100644
index 00000000..46552183
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/TaskDate.java
@@ -0,0 +1,35 @@
+package com.lockarhythm.tasks;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.List;
+import org.ocpsoft.prettytime.nlp.PrettyTimeParser;
+
+class TaskDate implements Serializable {
+ private LocalDateTime d;
+
+ public TaskDate(String s) {
+ List dates = new PrettyTimeParser().parse(s);
+ if (dates.size() > 0) {
+ d = toLocalDateTime(dates.get(0));
+ return;
+ }
+ d = LocalDateTime.parse(s);
+ }
+
+ public static int compare(TaskDate x, TaskDate y) {
+ return x.d.compareTo(y.d);
+ }
+
+ @Override
+ public String toString() {
+ return d.format(DateTimeFormatter.ofPattern("MMM d yyyy, HH:mm"));
+ }
+
+ private LocalDateTime toLocalDateTime(Date dateToConvert) {
+ return LocalDateTime.ofInstant(dateToConvert.toInstant(), ZoneId.systemDefault());
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java b/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java
new file mode 100644
index 00000000..d157e563
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/TaskDeserializer.java
@@ -0,0 +1,44 @@
+package com.lockarhythm.tasks;
+
+import com.google.gson.*;
+import com.lockarhythm.storage.GsonLocalDate;
+import com.lockarhythm.storage.GsonLocalDateTime;
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Technique credit to https://www.baeldung.com/gson-list */
+public class TaskDeserializer implements JsonDeserializer {
+ private String taskTypeElementName;
+ private Gson gson;
+ private Map> taskTypeRegistry;
+
+ public TaskDeserializer(String taskTypeElementName) {
+ this.gson =
+ new GsonBuilder()
+ .registerTypeAdapter(LocalDateTime.class, new GsonLocalDateTime())
+ .registerTypeAdapter(LocalDate.class, new GsonLocalDate())
+ .create();
+
+ this.taskTypeElementName = taskTypeElementName;
+ this.taskTypeRegistry = new HashMap<>();
+
+ this.registerTaskType("DEADLINE", DeadlineTask.class);
+ this.registerTaskType("TODO", TodoTask.class);
+ this.registerTaskType("EVENT", EventTask.class);
+ }
+
+ public void registerTaskType(String taskTypeName, Class extends Task> taskType) {
+ taskTypeRegistry.put(taskTypeName, taskType);
+ }
+
+ public Task deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ JsonObject taskObject = json.getAsJsonObject();
+ JsonElement taskTypeElement = taskObject.get(taskTypeElementName);
+
+ Class extends Task> taskType = taskTypeRegistry.get(taskTypeElement.getAsString());
+ return gson.fromJson(taskObject, taskType);
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/TaskList.java b/src/main/java/com/lockarhythm/tasks/TaskList.java
new file mode 100644
index 00000000..1709accc
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/TaskList.java
@@ -0,0 +1,100 @@
+package com.lockarhythm.tasks;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class TaskList {
+ private ArrayList list;
+
+ public TaskList() {
+ this.list = new ArrayList();
+ }
+
+ public TaskList(ArrayList list) {
+ this.list = list;
+ }
+
+ public int size() {
+ return list.size();
+ }
+
+ public TodoTask addTodoTask(String description) {
+ TodoTask task = new TodoTask(description);
+ list.add(task);
+ return task;
+ }
+
+ public DeadlineTask addDeadlineTask(String description, String by) {
+ DeadlineTask task = new DeadlineTask(description, by);
+ list.add(task);
+ return task;
+ }
+
+ public EventTask addEventTask(String description, String at) {
+ EventTask task = new EventTask(description, at);
+ list.add(task);
+ return task;
+ }
+
+ public Task markAsDone(int index) {
+ Task t = list.get(index);
+ t.setDone(true);
+ return t;
+ }
+
+ public Task deleteTask(int index) {
+ return list.remove(index);
+ }
+
+ /** Returns a copy of TaskList sorted by the task date. */
+ public TaskList sortByTaskDate(boolean isAscending) {
+ ArrayList copy = new ArrayList(list);
+ Collections.sort(
+ copy,
+ (a, b) -> {
+ if (isAscending) {
+ return TaskDate.compare(a.getTaskDate(), b.getTaskDate());
+ }
+ return TaskDate.compare(b.getTaskDate(), a.getTaskDate());
+ });
+ return new TaskList(copy);
+ }
+
+ /** Returns a copy of TaskList sorted by the "done" field. */
+ public TaskList sortByDone(boolean isAscending) {
+ ArrayList copy = new ArrayList(list);
+ Collections.sort(
+ copy,
+ (a, b) -> {
+ if (isAscending) {
+ return Boolean.compare(b.isDone(), a.isDone());
+ }
+ return Boolean.compare(a.isDone(), b.isDone());
+ });
+ return new TaskList(copy);
+ }
+
+ /** Returns a new TaskList that matches the given "query" string. */
+ public TaskList find(String query) {
+ ArrayList filtered = new ArrayList();
+ for (Task task : list) {
+ if (task.getDescription().contains(query)) {
+ filtered.add(task);
+ }
+ }
+ return new TaskList(filtered);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ int i = 1;
+ for (i = 0; i < list.size(); i++) {
+ s.append(i + 1);
+ s.append(".");
+ s.append(list.get(i));
+ s.append("\n");
+ }
+ return s.toString();
+ }
+}
diff --git a/src/main/java/com/lockarhythm/tasks/TodoTask.java b/src/main/java/com/lockarhythm/tasks/TodoTask.java
new file mode 100644
index 00000000..f7e2c263
--- /dev/null
+++ b/src/main/java/com/lockarhythm/tasks/TodoTask.java
@@ -0,0 +1,14 @@
+package com.lockarhythm.tasks;
+
+public final class TodoTask extends Task {
+ private String _type = "TODO";
+
+ public TodoTask(String description) {
+ super(description);
+ }
+
+ @Override
+ protected String getTaskTypeIcon() {
+ return "T";
+ }
+}
diff --git a/src/main/java/com/lockarhythm/ui/TerminalUI.java b/src/main/java/com/lockarhythm/ui/TerminalUI.java
new file mode 100644
index 00000000..084eea25
--- /dev/null
+++ b/src/main/java/com/lockarhythm/ui/TerminalUI.java
@@ -0,0 +1,54 @@
+package com.lockarhythm.ui;
+
+import com.lockarhythm.query.Result;
+import java.util.Arrays;
+import java.util.Scanner;
+
+/**
+ * TerminalUI is an adapter of UI port.
+ */
+public final class TerminalUI implements UI {
+ private static Scanner in = new Scanner(System.in);
+
+ /**
+ * nextLine reads the next line from standard input.
+ *
+ * @return the next line as a string
+ */
+ public String nextLine() {
+ return in.nextLine();
+ }
+
+ /**
+ * hasNext returns true if another line is available in standard input.
+ *
+ * @return true if another line is available. False otherwise.
+ */
+ public boolean hasNext() {
+ return in.hasNext();
+ }
+
+ /**
+ * print formats the given strings and prints to standard output.
+ *
+ * @param strings the list of strings to be printed
+ */
+ public void print(String... strings) {
+ System.out.println("\t____________________________________________________________");
+ for (String s : strings) {
+ s = Arrays.stream(s.split("\n")).map(x -> "\t" + x).reduce("", (x, y) -> x + y + "\n");
+ System.out.println(s);
+ }
+ System.out.println("\t____________________________________________________________\n");
+ }
+
+ /**
+ * print formats the given Result and prints to standard output. This is a helper function to handle the common operation of printing results from query interpreters.
+ *
+ * @param res the task Result to print.
+ */
+ public void print(Result res) {
+ assert res != null;
+ print(res.getText());
+ }
+}
diff --git a/src/main/java/com/lockarhythm/ui/UI.java b/src/main/java/com/lockarhythm/ui/UI.java
new file mode 100644
index 00000000..33c625f3
--- /dev/null
+++ b/src/main/java/com/lockarhythm/ui/UI.java
@@ -0,0 +1,36 @@
+package com.lockarhythm.ui;
+
+import com.lockarhythm.query.Result;
+
+/**
+ * UI is a port. An example adapter is the TerminalUI.
+ */
+public interface UI {
+ /**
+ * nextLine returns the next line from the input.
+ *
+ * @return the next line as a string.
+ */
+ public String nextLine();
+
+ /**
+ * hasNext returns true if there are more line(s) to be consumed in the next call to nextLine. Returns false otherwise.
+ *
+ * @return true if a new line exists. False otherwise.
+ */
+ public boolean hasNext();
+
+ /**
+ * print should print the given strings to the output.
+ *
+ * @param strings a list of strings to be printed
+ */
+ public void print(String... strings);
+
+ /**
+ * print is a helper function to print Results to output.
+ *
+ * @param res a task result to print.
+ */
+ public void print(Result res);
+}
diff --git a/src/test/java/com/lockarhythm/application/TestTerminalDuke.java b/src/test/java/com/lockarhythm/application/TestTerminalDuke.java
new file mode 100644
index 00000000..11d2f9b5
--- /dev/null
+++ b/src/test/java/com/lockarhythm/application/TestTerminalDuke.java
@@ -0,0 +1,36 @@
+package com.lockarhythm.application;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+import org.junit.Test;
+
+public class TestTerminalDuke {
+ private final PrintStream standardOut = System.out;
+ private final InputStream standardIn = System.in;
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ private final ByteArrayInputStream in =
+ new ByteArrayInputStream("todo read book\ntodo return book\nlist\nbye".getBytes());
+
+ @Test
+ public void testMeetsLevel2() throws Exception {
+ System.setIn(in);
+ System.setOut(new PrintStream(out));
+
+ TerminalDuke.main(null);
+ String output = out.toString().trim();
+ assertFalse(output.isEmpty());
+
+ assertTrue("it adds the 1st item", output.contains("Now you have 1 tasks in the list"));
+ assertTrue("it adds the 2nd item", output.contains("Now you have 2 tasks in the list"));
+ assertTrue("it lists the items", output.contains("1.[T][ ] read book"));
+ assertTrue("it lists the items", output.contains("2.[T][ ] return book"));
+ assertTrue("it exits", output.contains("Bye. Hope to see you again soon!"));
+
+ System.setOut(standardOut);
+ System.setIn(standardIn);
+ }
+}
diff --git a/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java b/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java
new file mode 100644
index 00000000..a528908a
--- /dev/null
+++ b/src/test/java/com/lockarhythm/query/echo/TestEchoResponder.java
@@ -0,0 +1,17 @@
+package com.lockarhythm.query.echo;
+
+import static org.junit.Assert.*;
+
+import com.lockarhythm.query.Result;
+import org.junit.Test;
+
+public class TestEchoResponder {
+ @Test
+ public void testEchosBackText() throws Exception {
+ EchoResponder responder = new EchoResponder();
+ Result res = responder.interpret("hello");
+
+ assertEquals("hello", res.getText());
+ assertFalse(res.shouldExit());
+ }
+}
diff --git a/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java b/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java
new file mode 100644
index 00000000..da25c270
--- /dev/null
+++ b/src/test/java/com/lockarhythm/query/exit/TestExitResponder.java
@@ -0,0 +1,24 @@
+package com.lockarhythm.query.exit;
+
+import static org.junit.Assert.*;
+
+import com.lockarhythm.query.Result;
+import org.junit.Test;
+
+public class TestExitResponder {
+ @Test
+ public void testExitsOnKeyword() throws Exception {
+ ExitResponder responder = new ExitResponder();
+ Result res = responder.interpret("bye");
+
+ assertTrue(res.shouldExit());
+ }
+
+ @Test
+ public void testNullOnNonkeyword() throws Exception {
+ ExitResponder responder = new ExitResponder();
+ Result res = responder.interpret("hello");
+
+ assertNull(res);
+ }
+}
diff --git a/src/test/java/com/lockarhythm/query/list/TestListResponder.java b/src/test/java/com/lockarhythm/query/list/TestListResponder.java
new file mode 100644
index 00000000..03f7c638
--- /dev/null
+++ b/src/test/java/com/lockarhythm/query/list/TestListResponder.java
@@ -0,0 +1,21 @@
+package com.lockarhythm.query.list;
+
+import static org.junit.Assert.*;
+
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.TaskList;
+import org.junit.Test;
+
+public class TestListResponder {
+ @Test
+ public void testAddsItemsAndLists() throws Exception {
+ TaskList list = new TaskList();
+ ListResponder responder = new ListResponder(list);
+
+ list.addTodoTask("read book");
+ list.addTodoTask("return book");
+
+ Result res = responder.interpret("list");
+ assertTrue(res.getText().contains("1.[T][ ] read book\n2.[T][ ] return book\n"));
+ }
+}
diff --git a/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java b/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java
new file mode 100644
index 00000000..9655d533
--- /dev/null
+++ b/src/test/java/com/lockarhythm/query/markasdone/TestMarkAsDoneResponder.java
@@ -0,0 +1,49 @@
+package com.lockarhythm.query.markasdone;
+
+import static org.junit.Assert.*;
+
+import com.lockarhythm.query.Result;
+import com.lockarhythm.tasks.TaskList;
+import org.junit.Test;
+
+public class TestMarkAsDoneResponder {
+ @Test
+ public void testMarkAsDoneHappyPath() throws Exception {
+ TaskList list = new TaskList();
+ MarkAsDoneResponder responder = new MarkAsDoneResponder(list);
+
+ list.addTodoTask("read book");
+ list.addTodoTask("return book");
+
+ Result res = responder.interpret("done 1");
+ assertTrue(res.getText().contains("Nice! I've marked this task as done"));
+ assertTrue(res.getText().contains("read book"));
+
+ assertTrue(list.toString().contains("1.[T][X] read book\n2.[T][ ] return book\n"));
+ }
+
+ @Test
+ public void testMarkAsDoneOnNonExistentItem() throws Exception {
+ TaskList list = new TaskList();
+ MarkAsDoneResponder responder = new MarkAsDoneResponder(list);
+
+ list.addTodoTask("read book");
+ list.addTodoTask("return book");
+
+ Result res = responder.interpret("done 10000");
+ assertTrue(res.getText().contains("Item 10000 is not on the list. I cannot mark it as done!"));
+
+ assertTrue(list.toString().contains("1.[T][ ] read book\n2.[T][ ] return book\n"));
+ }
+
+ @Test
+ public void testMarkAsDoneParseCommand() throws Exception {
+ TaskList list = new TaskList();
+ MarkAsDoneResponder responder = new MarkAsDoneResponder(list);
+
+ list.addTodoTask("read book");
+
+ Result res = responder.interpret("all done 10000");
+ assertNull(res);
+ }
+}
diff --git a/src/test/java/com/lockarhythm/tasks/TestTaskList.java b/src/test/java/com/lockarhythm/tasks/TestTaskList.java
new file mode 100644
index 00000000..5c98bd5c
--- /dev/null
+++ b/src/test/java/com/lockarhythm/tasks/TestTaskList.java
@@ -0,0 +1,23 @@
+package com.lockarhythm.tasks;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class TestTaskList {
+ @Test
+ public void testAddTask() throws Exception {
+ TaskList list = new TaskList();
+
+ list.addTodoTask("learn useful libraries in java");
+ list.addTodoTask("learn bazel");
+ assertEquals("has correct number of items", 2, list.size());
+
+ Task task = list.markAsDone(1);
+ assertEquals("can mark tasks as done", "learn bazel", task.getDescription());
+ assertTrue("task is set to done", task.isDone());
+
+ task = list.markAsDone(1);
+ assertTrue("it's an idempotent operation", task.isDone());
+ }
+}
diff --git a/src/test/java/com/lockarhythm/tasks/TestTodoTask.java b/src/test/java/com/lockarhythm/tasks/TestTodoTask.java
new file mode 100644
index 00000000..a9fb2591
--- /dev/null
+++ b/src/test/java/com/lockarhythm/tasks/TestTodoTask.java
@@ -0,0 +1,15 @@
+package com.lockarhythm.tasks;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class TestTodoTask {
+ @Test
+ public void testToggleDone() throws Exception {
+ TodoTask task = new TodoTask("learn java well");
+
+ task.toggleDone();
+ assertTrue("task should be done", task.isDone());
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 657e74f6..11540f9b 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,7 +1,231 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
+ ____________________________________________________________
+ Hello I'm
+ ____ _
+ | _ \ _ _| | _____
+ | | | | | | | |/ / _ \
+ | |_| | |_| | < __/
+ |____/ \__,_|_|\_\___|
+
+ What can I do for you?
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [T][ ] borrow book
+ Now you have 1 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [D][ ] return book (by: Oct 25 2021, 22:00)
+ Now you have 2 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [E][ ] buy bread (at: Oct 25 2021, 08:00)
+ Now you have 3 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [D][ ] return book (by: Dec 31 2021, 23:59)
+ Now you have 4 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [E][ ] buy bread (at: Nov 30 2021, 23:59)
+ Now you have 5 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Nice! I've marked this task as done:
+ [T][X] borrow book
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Here are the tasks in your list:
+ 1.[T][X] borrow book
+ 2.[D][ ] return book (by: Oct 25 2021, 22:00)
+ 3.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 4.[D][ ] return book (by: Dec 31 2021, 23:59)
+ 5.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Nice! I've marked this task as done:
+ [D][X] return book (by: Oct 25 2021, 22:00)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Nice! I've marked this task as done:
+ [D][X] return book (by: Oct 25 2021, 22:00)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [T][ ] done 1
+ Now you have 6 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Here are the tasks in your list:
+ 1.[T][X] borrow book
+ 2.[D][X] return book (by: Oct 25 2021, 22:00)
+ 3.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 4.[D][ ] return book (by: Dec 31 2021, 23:59)
+ 5.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 6.[T][ ] done 1
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Sorry, I don't understand that yet!
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Item 0 is not on the list. I cannot delete it!
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Noted. I've removed this task:
+ [D][X] return book (by: Oct 25 2021, 22:00)
+ Now you have 5 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ Now you have 6 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Got it. I've added this task:
+ [E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+ Now you have 7 tasks in the list.
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have sorted the tasks by date in ascending order!
+ 1.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+ 2.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 3.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[D][ ] return book (by: Dec 31 2021, 23:59)
+ 6.[T][X] borrow book
+ 7.[T][ ] done 1
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have sorted the tasks by date in descending order!
+ 1.[T][X] borrow book
+ 2.[T][ ] done 1
+ 3.[D][ ] return book (by: Dec 31 2021, 23:59)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Here are the tasks in your list:
+ 1.[T][X] borrow book
+ 2.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 3.[D][ ] return book (by: Dec 31 2021, 23:59)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[T][ ] done 1
+ 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Nice! I've marked this task as done:
+ [D][X] return book (by: Dec 31 2021, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have sorted the tasks by done in ascending order!
+ 1.[T][X] borrow book
+ 2.[D][X] return book (by: Dec 31 2021, 23:59)
+ 3.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[T][ ] done 1
+ 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have sorted the tasks by done in ascending order!
+ 1.[T][X] borrow book
+ 2.[D][X] return book (by: Dec 31 2021, 23:59)
+ 3.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[T][ ] done 1
+ 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have sorted the tasks by done in descending order!
+ 1.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 2.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 3.[T][ ] done 1
+ 4.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 5.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+ 6.[T][X] borrow book
+ 7.[D][X] return book (by: Dec 31 2021, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Here are the tasks in your list:
+ 1.[T][X] borrow book
+ 2.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 3.[D][X] return book (by: Dec 31 2021, 23:59)
+ 4.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+ 5.[T][ ] done 1
+ 6.[E][ ] this task is so 2008 (at: Oct 31 2008, 23:59)
+ 7.[E][ ] this task is so 2000-and-late (at: Dec 31 2000, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Ok! I have found these tasks for your query 'bread'!
+ 1.[E][ ] buy bread (at: Oct 25 2021, 08:00)
+ 2.[E][ ] buy bread (at: Nov 30 2021, 23:59)
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ I could not find any tasks matching your query 'answer2life'
+
+ ____________________________________________________________
+
+ ____________________________________________________________
+ Bye. Hope to see you again soon!
+
+ ____________________________________________________________
diff --git a/text-ui-test/EXPECTED.json b/text-ui-test/EXPECTED.json
new file mode 100644
index 00000000..86afcdee
--- /dev/null
+++ b/text-ui-test/EXPECTED.json
@@ -0,0 +1,52 @@
+[
+ {
+ "_type": "TODO",
+ "description": "borrow book",
+ "isDone": true
+ },
+ {
+ "at": {
+ "d": "2021-10-25T08:00:00"
+ },
+ "_type": "EVENT",
+ "description": "buy bread",
+ "isDone": false
+ },
+ {
+ "by": {
+ "d": "2021-12-31T23:59:59"
+ },
+ "_type": "DEADLINE",
+ "description": "return book",
+ "isDone": true
+ },
+ {
+ "at": {
+ "d": "2021-11-30T23:59:59"
+ },
+ "_type": "EVENT",
+ "description": "buy bread",
+ "isDone": false
+ },
+ {
+ "_type": "TODO",
+ "description": "done 1",
+ "isDone": false
+ },
+ {
+ "at": {
+ "d": "2008-10-31T23:59:59"
+ },
+ "_type": "EVENT",
+ "description": "this task is so 2008",
+ "isDone": false
+ },
+ {
+ "at": {
+ "d": "2000-12-31T23:59:59"
+ },
+ "_type": "EVENT",
+ "description": "this task is so 2000-and-late",
+ "isDone": false
+ }
+]
\ No newline at end of file
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index e69de29b..1d8120d4 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -0,0 +1,27 @@
+todo borrow book
+deadline return book /by Oct 25 10pm
+event buy bread /at Oct 25 morning
+deadline return book /by 2021-12-31T23:59:59
+event buy bread /at 2021-11-30T23:59:59
+done 1
+list
+done 2
+done 2
+todo done 1
+list
+a-command-that-duke-doesnt-know
+delete 0
+delete 2
+event this task is so 2008 /at 2008-10-31T23:59:59
+event this task is so 2000-and-late /at 2000-12-31T23:59:59
+sort by date
+sort by date desc
+list
+done 3
+sort by done
+sort by done asc
+sort by done desc
+list
+find bread
+find answer2life
+bye
diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh
old mode 100644
new mode 100755
index c9ec8700..bbc3283f
--- a/text-ui-test/runtest.sh
+++ b/text-ui-test/runtest.sh
@@ -12,15 +12,21 @@ then
rm ACTUAL.TXT
fi
+# delete tasks.json from previous run
+if [ -e "./tasks.json" ]
+then
+ rm tasks.json
+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 ! USE_BAZEL_VERSION=ac9353fab161efae4af72e73fbb657a762b3620d bazelisk build //:TerminalDukeQA
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
+../bazel-bin/TerminalDukeQA < input.txt > ACTUAL.TXT
# convert to UNIX format
cp EXPECTED.TXT EXPECTED-UNIX.TXT
@@ -30,9 +36,19 @@ dos2unix ACTUAL.TXT EXPECTED-UNIX.TXT
diff ACTUAL.TXT EXPECTED-UNIX.TXT
if [ $? -eq 0 ]
then
- echo "Test result: PASSED"
+ echo "Test result: PASSED [UI TESTS]"
+else
+ echo "Test result: FAILED [UI TESTS]"
+ exit 1
+fi
+
+# compare the output to the expected output
+diff tasks.json EXPECTED.json
+if [ $? -eq 0 ]
+then
+ echo "Test result: PASSED [SERIALIZATION TESTS]"
exit 0
else
- echo "Test result: FAILED"
+ echo "Test result: FAILED [SERIALIZATION TESTS]"
exit 1
-fi
\ No newline at end of file
+fi
diff --git a/tools/format-code.sh b/tools/format-code.sh
new file mode 100644
index 00000000..da44ba37
--- /dev/null
+++ b/tools/format-code.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env sh
+mkdir -p .cache
+cd .cache
+if [ ! -f google-java-format-1.7-all-deps.jar ]
+then
+ curl -LJO "https://github.com/google/google-java-format/releases/download/google-java-format-1.7/google-java-format-1.7-all-deps.jar"
+ chmod 755 google-java-format-1.7-all-deps.jar
+fi
+cd ..
+
+changed_java_files=$(git diff --cached --name-only --diff-filter=ACMR | grep ".*java$" )
+echo $changed_java_files
+java -jar .cache/google-java-format-1.7-all-deps.jar --replace $changed_java_files