diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000000..345ee573e3 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,34 @@ +name: Java CI + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up repository + uses: actions/checkout@master + + - name: Set up repository + uses: actions/checkout@master + with: + ref: master + + - name: Merge to master + run: git checkout --progress --force ${{ github.sha }} + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Setup JDK 17 + uses: actions/setup-java@v1 + with: + java-version: '17' + java-package: jdk+fx + + - name: Build and check with Gradle + run: ./gradlew check \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2873e189e1..9654ef4575 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +/data/ diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000000..ebdd190cb0 --- /dev/null +++ b/_config.yml @@ -0,0 +1,3 @@ +remote_theme: pages-themes/tactile@v0.2.0 +plugins: +- jekyll-remote-theme \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..bb99c12163 --- /dev/null +++ b/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'checkstyle' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + + String javaFxVersion = '17.0.7' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("lawrence.ui.Launcher") +} + +shadowJar { + archiveFileName = "lawrence.jar" +} + +checkstyle { + toolVersion = '10.2' +} + +run { + standardInput = System.in +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..acac1a8e28 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..135ea49ee0 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..9b7b267807 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,140 @@ -# Duke User Guide +# Lawrence User Guide -// Update the title above to match the actual product name +Lawrence is a chatbot designed to help you keep track of your tasks. +It offers a myriad of features to ensure that your productivity is unaffected even when faced with high workloads. -// Product screenshot goes here +![](Ui.png) -// Product intro goes here +This is a list of features that Lawrence offers: +1. Adding of Tasks +2. Updating of Task Completion Statuses +3. Deleting of Tracked Tasks +4. Viewing Tracked Tasks +5. Searching for Tasks using Keywords -## Adding deadlines +# Feature Usage +## Adding Tasks +The bot is able to track 3 types of tasks: Todos, Deadlines and Events. -// Describe the action and its outcome. +### Adding Todos +Todos are the simplest of all tasks and are not associated with any time period. -// Give examples of usage +To get Lawrence to track a todo, use the `todo` command. +Usage: +``` +todo +``` +If successful, the chatbot will respond with a readback of the added Todo. + +### Adding Deadlines +Deadlines are similar to Todos, but require an extra date parameter. + +To get Lawrence to track a deadline use the `deadline` command. +Usage: +``` +deadline /by +``` +If successful, the chatbot will respond with a readback of the added Deadline. + +### Adding Events +Events are similar to Deadlines, but require a start date and end date. + +To get Lawrence to track an Event, use the `event` command. +Usage: +``` +event /from /to +``` +If successful, the chatbot will respond with a readback of the added Event. + +### Successful Add +If the command was executed successfully, the bot should respond with a readback of the added Task. +``` +Alright, added to the list. +There are currently task(s) in the list. +``` +### Unsucccessful Add +If there was any error when processing the task to be added, the bot will attempt to diagnose and inform you of the error in the command. + +## Updating Completion Statuses +Existing tasks can be marked as complete or incomplete using the `mark` and `unmark` commands respectively. + +Usage: +``` +mark +unmark +``` -Example: `keyword (optional arguments)` +Examples: +- `mark 1` will mark the first task in the list as complete +- `unmark 2` will mark the second task in the list as incomplete. -// A description of the expected outcome goes here +If the specified task number does not exist, the bot will not proceed with the request. +Examples of invalid task numbers: +- `mark -1` +- `unmark 6` when there are only `5` tasks in the list. +## Deleting Tasks +Existing tasks can be deleted using the `delete` command with a specified task number. +Usage: ``` -expected output +delete ``` -## Feature ABC +Example: +- `delete 1` will delete the first task in the list. + +If the specified task number does not exist, the bot will not proceed with the request. +Examples of invalid task numbers: +- `delete -1` +- `delete 6` when there are only `5` tasks in the list. + +## Viewing Tasks in the List +The tasks currently tracked by Lawrence can be viewed using the `list` command. +Usage: +``` +list +``` + +## Searching for Tasks +The tasks currently tracked by Lawrence can be searched by their descriptions. +To do so, use the `find` command. + +Usage: +``` +find +``` + +The bot will consolidate and display the tasks that match the specified keywords. +Example: `find organise` will return all tasks that have the keyword `organise` in their task description. + +If no matches are found, the bot will also notify you of the result. + +## Exiting the Program +To exit the program, use the `bye` command to terminate the chatbot session. +Usage: +``` +bye +``` + +The tasks added are automatically saved and will persist between chatbot sessions. + +# Lenient Syntax +The bot is able to recognise **some** abbreviated commands to reduce the amount of typing the user needs to do. +Commands can be recognised without fully spelling out the command. + +Examples include: +- `t` and `to` can be recognised as the `todo` command +- `m` can be recognised as the `mark` command +- `u` can be recognised as the `unmark` command -// Feature details +## Ambiguity +However, it should be noted that the `delete` and `deadline` command can be ambiguous if shortened to a single letter. +As such, the bot will refuse to process commands such as `d` as it may mean two different things. -## Feature XYZ +Luckily, the bot is still able to recognise a slightly longer but still shortened form of these commands: +- `del` is recognised as `delete` +- `deadl` is recognised as `deadline` -// Feature details \ No newline at end of file +# Dealing with Errors +If there was any problem with the execution of a command, the bot will attempt to diagnose it and let you know of any modifications that need to be made to the command. \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..9d2a5028d3 Binary files /dev/null and b/docs/Ui.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..033e24c4cd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..66c01cfeba --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..fcb6fca147 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..6689b85bee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- 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/lawrence/app/Lawrence.java b/src/main/java/lawrence/app/Lawrence.java new file mode 100644 index 0000000000..d62a3beb0e --- /dev/null +++ b/src/main/java/lawrence/app/Lawrence.java @@ -0,0 +1,106 @@ +package lawrence.app; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Scanner; + +import lawrence.command.Command; +import lawrence.command.CommandType; +import lawrence.database.TaskFileManager; +import lawrence.parser.CommandParser; +import lawrence.task.Task; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * The Lawrence chatbot that is able to keep track of tasks and their + * respective completion statuses. + *

+ * The tasks are saved automatically after every action. + * This ensures that all changes are preserved and will be available + * the next time the program is run. + *

+ * @author Kok Bo Chang + */ +public class Lawrence { + private static final String NAME = "Lawrence"; + private static final Path SAVE_LOCATION = Paths.get(".", "data", "tasks.txt"); + + private final TaskFileManager manager; + private TaskList tasks; + private final UserInterface ui; + + /** + * Default constructor. + */ + public Lawrence() { + ui = new UserInterface(NAME); + manager = new TaskFileManager(SAVE_LOCATION); + try { + Task[] existingTasks = manager.readTasksFromFile(); + tasks = new TaskList(existingTasks); + } catch (IOException e) { + // initialise with no tasks instead + tasks = new TaskList(); + } + + assert tasks != null; // the task object will always be initialised + } + + /** + * Initialises and runs the Lawrence chatbot. + * + * @param args optional startup arguments + */ + public static void main(String[] args) { + Lawrence lawrence = new Lawrence(); + lawrence.run(); + } + + /** + * Runs the chatbot to start listening for user commands + * in the console. + */ + public void run() { + ui.greetUser(); + + Scanner sc = new Scanner(System.in); + String userInput; + boolean shouldContinue = true; + while (shouldContinue) { + userInput = sc.nextLine(); + try { + Command c = CommandParser.createCommand(userInput); + c.execute(tasks, manager, ui); + shouldContinue = c.shouldContinue(); + } catch (IllegalArgumentException | IllegalStateException e) { + ui.showMessage(String.format("%s Please try again.", e.getMessage())); + } + } + + assert shouldContinue == false; // program should only exit once shouldContinue is false + } + + /** + * Returns the text response of the bot after parsing the input string and executing the relevant command. + * + * @param input the input string containing instructions on what command to run + * @return a {@link Response} object containing details on the actions the bot took + */ + public Response getResponse(String input) { + try { + Command c = CommandParser.createCommand(input); + String message = c.execute(tasks, manager, ui); + return new Response(c.getType(), message, c.shouldContinue()); + } catch (IllegalArgumentException | IllegalStateException e) { + String message = String.format("%s Please try again.", e.getMessage()); + return new Response(CommandType.INVALID, message, true); + } + } + + public String getWelcomeMessage() { + return String.format("Hello! I'm %s and I'm here to establish another GST hike.%n" + + "What can I do for you?", NAME); + } +} diff --git a/src/main/java/lawrence/app/Response.java b/src/main/java/lawrence/app/Response.java new file mode 100644 index 0000000000..264c1fd571 --- /dev/null +++ b/src/main/java/lawrence/app/Response.java @@ -0,0 +1,9 @@ +package lawrence.app; + +import lawrence.command.CommandType; + +/** + * Contains information about the action taken by the bot after issuing a command to it. + */ +public record Response(CommandType commandType, String message, boolean shouldContinue) { +} diff --git a/src/main/java/lawrence/command/AddTaskCommand.java b/src/main/java/lawrence/command/AddTaskCommand.java new file mode 100644 index 0000000000..fa99f47ef7 --- /dev/null +++ b/src/main/java/lawrence/command/AddTaskCommand.java @@ -0,0 +1,76 @@ +package lawrence.command; + +import java.io.IOException; +import java.time.format.DateTimeParseException; + +import lawrence.database.TaskFileManager; +import lawrence.parser.InputSource; +import lawrence.parser.TaskParser; +import lawrence.task.Task; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; +import lawrence.utils.DateParser; + +/** + * Represents the user command to create new tasks. + */ +public class AddTaskCommand extends Command { + private final String input; + + /** + * Default constructor. + * + * @param input the user input associated with this command + */ + public AddTaskCommand(CommandType type, String input) { + super(type); + this.input = input; + } + + /** + * Creates the relevant task based on user input, then adds + * the new task into the {@link TaskList} and saves the + * tasks into a text file. + *

+ * Displays the result of the execution to the user afterwards. + *

+ *

+ * If information about the task to add is invalid, + * no new task will be created and the text file will not be updated. + *

+ * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + try { + Task t = TaskParser.createTask(input, InputSource.USER); + tasks.addTask(t); + saveTasks(tasks, manager); + + // format components of message to display + int numberOfTasks = tasks.getSize(); + + assert numberOfTasks > 0; // successfully adding a task means tasks will always have a length of at least 1 + + String verb = numberOfTasks == 1 ? "is" : "are"; + String plural = numberOfTasks == 1 ? "" : "s"; + return String.format("Alright, added task:%n%s%nto the list.%n" + + "There %s currently %d task%s in the list.", t, verb, numberOfTasks, plural); + } catch (DateTimeParseException e) { + return String.format("Invalid date and/or time provided. DateTime should be in the format: %s", + DateParser.FORMAT_STRING_FOR_USER_INPUT); + } catch (IllegalArgumentException e) { + return e.getMessage(); + } catch (IOException e) { + return "Failed to save tasks to file. Please try again."; + } + } +} diff --git a/src/main/java/lawrence/command/Command.java b/src/main/java/lawrence/command/Command.java new file mode 100644 index 0000000000..da02933fb9 --- /dev/null +++ b/src/main/java/lawrence/command/Command.java @@ -0,0 +1,71 @@ +package lawrence.command; + +import java.io.IOException; + +import lawrence.database.TaskFileManager; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents a general command that can be issued by the user. + *

+ * The command is not processed until + * {@link #execute(TaskList, TaskFileManager, UserInterface)} + * is called. + *

+ */ +public abstract class Command { + private final CommandType type; + + protected Command(CommandType type) { + this.type = type; + } + /** + * Executes the specified user command. + * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + public abstract String execute(TaskList tasks, TaskFileManager manager, UserInterface ui); + + /** + * Returns a boolean indicating whether the program should + * continue running after this command. Defaults to true. + * + * @return a boolean indicating whether the program should + * continue running + */ + public boolean shouldContinue() { + return true; + } + + /** + * Saves the current tasks in memory to a text file. + *

+ * This operation will overwrite the original text file entirely. + *

+ * + * @param tasks the list of tasks to be saved into the text file + * @param manager the {@link TaskFileManager} instance that + * can write to the text file + * @throws IOException if writing to the file is unsuccessful + */ + protected void saveTasks(TaskList tasks, TaskFileManager manager) throws IOException { + manager.saveTasksToFile(tasks.getTasks()); + } + + /** + * Returns the type of the current command. + * + * @return the command type + */ + public CommandType getType() { + return type; + } +} diff --git a/src/main/java/lawrence/command/CommandType.java b/src/main/java/lawrence/command/CommandType.java new file mode 100644 index 0000000000..175f7b8576 --- /dev/null +++ b/src/main/java/lawrence/command/CommandType.java @@ -0,0 +1,80 @@ +package lawrence.command; + +import java.util.ArrayList; + +/** + * Represents the different commands that can be issued by the user. + */ +public enum CommandType { + ADD_DEADLINE("deadline"), + ADD_EVENT("event"), + ADD_TODO("todo"), + DELETE("delete"), + DISPLAY("list"), + EXIT("bye"), + FIND_MATCHING("find"), + INVALID("invalid"), + MARK_COMPLETE("mark"), + MARK_INCOMPLETE("unmark"); + + private final String commandType; + + /** + * Default constructor. + *

+ * The input string is converted into lowercase for greater input flexibility. + *

+ * + * @param type the string containing an enum value + */ + CommandType(String type) { + this.commandType = type.toLowerCase(); + } + + /** + * Converts a text string into its relevant enum counterpart. Full string matching + * and partial string matching are both done. + *

+ * If input does not match any known types, the {@link #INVALID} type + * is returned. + * IF the input matches multiple types, the {@link #INVALID} type is also returned. + *

+ * + * @param input the text containing an enum value + * @return an enum type matching the input + */ + public static CommandType fromString(String input) { + ArrayList matches = new ArrayList<>(); + for (CommandType type : CommandType.values()) { + // do not attempt to match with type INVALID as it is reserved to signify a failed parse + if (type == INVALID) { + continue; + } + + String typeString = type.getCommandType(); + if (typeString.equalsIgnoreCase(input)) { + matches.add(type); + continue; + } + + // Check for partial match + if (typeString.startsWith(input.toLowerCase())) { + matches.add(type); + } + } + + if (matches.size() != 1) { + return INVALID; + } + return matches.get(0); + } + + /** + * Returns the command type as a string. + * + * @return a string of the command type + */ + public String getCommandType() { + return commandType; + } +} diff --git a/src/main/java/lawrence/command/CompleteTaskCommand.java b/src/main/java/lawrence/command/CompleteTaskCommand.java new file mode 100644 index 0000000000..55daee4038 --- /dev/null +++ b/src/main/java/lawrence/command/CompleteTaskCommand.java @@ -0,0 +1,66 @@ +package lawrence.command; + +import java.io.IOException; + +import lawrence.database.TaskFileManager; +import lawrence.task.Task; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to mark existing tasks as complete. + */ +public class CompleteTaskCommand extends Command { + private final String input; + + /** + * Default constructor. + * + * @param input the user input associated with this command + */ + public CompleteTaskCommand(CommandType type, String input) { + super(type); + this.input = input; + } + + /** + * Finds the specified task and marks it as complete, then displays + * the new status of the task to the user. + *

+ * If information about the task to mark complete is invalid, the + * method does nothing. + *

+ * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + String[] inputComponents = input.split(" ", 2); + if (inputComponents.length < 2) { + return "Please specify the task you want to mark as complete."; + } + + assert inputComponents.length == 2; + + String rawTaskNumber = inputComponents[1]; + try { + int taskNumber = Integer.parseInt(rawTaskNumber); + Task completeTask = tasks.completeTask(taskNumber); + saveTasks(tasks, manager); + return String.format("I've marked the task as complete:%n%s", completeTask); + } catch (NumberFormatException e) { + return "Please specify a number to select a task."; + } catch (IllegalArgumentException | IllegalStateException e) { + return String.format("%s Please try again.", e.getMessage()); + } catch (IOException e) { + return String.format("Failed to mark task %s as complete. Please try again later.", rawTaskNumber); + } + } +} diff --git a/src/main/java/lawrence/command/DeleteTaskCommand.java b/src/main/java/lawrence/command/DeleteTaskCommand.java new file mode 100644 index 0000000000..07383aae4f --- /dev/null +++ b/src/main/java/lawrence/command/DeleteTaskCommand.java @@ -0,0 +1,66 @@ +package lawrence.command; + +import java.io.IOException; + +import lawrence.database.TaskFileManager; +import lawrence.task.Task; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to delete existing tasks from the list. + */ +public class DeleteTaskCommand extends Command { + private final String input; + + /** + * Default constructor. + * + * @param input the user input associated with this command + */ + public DeleteTaskCommand(CommandType type, String input) { + super(type); + this.input = input; + } + + /** + * Finds the specified task and deletes it from the list, then + * displays the new list after deletion to the user. + *

+ * If information about the task to be deleted is invalid, + * the method does nothing. + *

+ * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + String[] inputComponents = input.split(" ", 2); + if (inputComponents.length < 2) { + return "Please specify the task you want to delete."; + } + + String rawTaskNumber = inputComponents[1]; + try { + int taskNumber = Integer.parseInt(rawTaskNumber); + Task deletedTask = tasks.deleteTask(taskNumber); + saveTasks(tasks, manager); + + return String.format("Task:%n%s%nhas been deleted.", deletedTask); + } catch (NumberFormatException e) { + return "Please specify an integer to select a task."; + } catch (IllegalArgumentException | IllegalStateException e) { + return String.format("%s Please try again.", e.getMessage()); + } catch (IOException e) { + return String.format("Failed to delete task %s from the list. Please try again.", rawTaskNumber); + } + } + +} diff --git a/src/main/java/lawrence/command/DisplayTasksCommand.java b/src/main/java/lawrence/command/DisplayTasksCommand.java new file mode 100644 index 0000000000..d0972826a1 --- /dev/null +++ b/src/main/java/lawrence/command/DisplayTasksCommand.java @@ -0,0 +1,40 @@ +package lawrence.command; + +import lawrence.database.TaskFileManager; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to display all existing tasks in the list. + */ +public class DisplayTasksCommand extends Command { + /** + * Default constructor. + */ + public DisplayTasksCommand(CommandType type) { + super(type); + } + + /** + * Displays all tasks present in the {@link TaskList} to the user. + * If no tasks exist, a different message is displayed. + * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + if (tasks.getSize() < 1) { + return "You have no tasks at the moment."; + } + + assert tasks.getSize() >= 0; + return String.format("Here's your laundry list:%n%s", tasks); + } +} diff --git a/src/main/java/lawrence/command/ExitSessionCommand.java b/src/main/java/lawrence/command/ExitSessionCommand.java new file mode 100644 index 0000000000..b86d91164f --- /dev/null +++ b/src/main/java/lawrence/command/ExitSessionCommand.java @@ -0,0 +1,46 @@ +package lawrence.command; + +import lawrence.database.TaskFileManager; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to exit the chatbot. + */ +public class ExitSessionCommand extends Command { + /** + * Default constructor. + */ + public ExitSessionCommand(CommandType type) { + super(type); + } + + /** + * Displays an exit message to the user. + * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + return "That's all folks! Hope to see you again soon!"; + } + + /** + * Returns a boolean indicating whether the program should + * continue running after this command. Defaults to false. + * + * @return a boolean indicating whether the program should + * continue running; always false + */ + @Override + public boolean shouldContinue() { + return false; + } +} diff --git a/src/main/java/lawrence/command/FindTasksCommand.java b/src/main/java/lawrence/command/FindTasksCommand.java new file mode 100644 index 0000000000..88fd20e786 --- /dev/null +++ b/src/main/java/lawrence/command/FindTasksCommand.java @@ -0,0 +1,63 @@ +package lawrence.command; + +import lawrence.database.TaskFileManager; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to search for tasks through the task description. + */ +public class FindTasksCommand extends Command { + private final String input; + + /** + * Default constructor. + * + * @param input the user input associated with this command + */ + public FindTasksCommand(CommandType type, String input) { + super(type); + this.input = input; + } + + /** + * Finds tasks that match the user query and displays them. Queries that + * partially match task descriptions are also returned. + *

+ * Leading and trailing spaces are removed from the query to sanitise inputs. + * If the query is empty or only contains spaces, the user is notified and + * the method returns. + *

+ * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + if (input.isEmpty()) { + return "Match query cannot be empty!"; + } + + String[] inputComponents = input.split(" ", 2); + if (inputComponents.length < 2) { + return "Please include a phrase for your query."; + } + + String query = inputComponents[1].trim(); + if (query.isEmpty()) { + return "Please include a phrase for your query."; + } + TaskList results = tasks.findTasks(query); + if (results.getSize() < 1) { + return "No matches were found for your query: " + query; + } + + return String.format("Here are the matching tasks in your list:%n%s", results); + } +} diff --git a/src/main/java/lawrence/command/UncompleteTaskCommand.java b/src/main/java/lawrence/command/UncompleteTaskCommand.java new file mode 100644 index 0000000000..f2b6999189 --- /dev/null +++ b/src/main/java/lawrence/command/UncompleteTaskCommand.java @@ -0,0 +1,65 @@ +package lawrence.command; + +import java.io.IOException; + +import lawrence.database.TaskFileManager; +import lawrence.task.Task; +import lawrence.task.TaskList; +import lawrence.ui.UserInterface; + +/** + * Represents the user command to mark existing tasks as incomplete. + */ +public class UncompleteTaskCommand extends Command { + private final String input; + + /** + * Default constructor. + * + * @param input the user input associated with this command + */ + public UncompleteTaskCommand(CommandType type, String input) { + super(type); + this.input = input; + } + + /** + * Finds the specified task and marks it as incomplete, then displays + * the new status of the task to the user. + *

+ * If information about the task to mark incomplete is invalid, the + * method does nothing. + *

+ * + * @param tasks a list of tasks the command may operate + * on + * @param manager a {@link TaskFileManager} instance that + * the command may use when saving changes + * made + * @param ui a {@link UserInterface} instance to display + * possible messages to the user + * @return a string representing the bot's response after execution of the command + */ + @Override + public String execute(TaskList tasks, TaskFileManager manager, UserInterface ui) { + String[] inputComponents = input.split(" ", 2); + if (inputComponents.length < 2) { + return "Please specify the task you want to mark as incomplete."; + } + + String rawTaskNumber = inputComponents[1]; + try { + int taskNumber = Integer.parseInt(rawTaskNumber); + Task incompleteTask = tasks.uncompleteTask(taskNumber); + saveTasks(tasks, manager); + + return String.format("Changed your mind? The task is set to incomplete:%n%s", incompleteTask); + } catch (NumberFormatException e) { + return "Please specify a number to select a task."; + } catch (IllegalArgumentException | IllegalStateException e) { + return String.format("%s Please try again.", e.getMessage()); + } catch (IOException e) { + return String.format("Failed to mark task %s as incomplete. Please try again later.", rawTaskNumber); + } + } +} diff --git a/src/main/java/lawrence/database/TaskFileManager.java b/src/main/java/lawrence/database/TaskFileManager.java new file mode 100644 index 0000000000..7b2ffab57a --- /dev/null +++ b/src/main/java/lawrence/database/TaskFileManager.java @@ -0,0 +1,130 @@ +package lawrence.database; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +import lawrence.parser.InputSource; +import lawrence.parser.TaskParser; +import lawrence.task.Task; + +/** + * Interfaces with a text file to read and write tasks input by the user. + *

+ * The contents in the file persist even after the program terminates. + *

+ */ +public class TaskFileManager { + private final File file; + + /** + * Default constructor. The {@link Path} instance provided can be a + * relative or absolute path. + * + * @param path the path to the text file used to store tasks + */ + public TaskFileManager(Path path) { + file = path.toAbsolutePath() + .normalize() + .toFile(); + } + + /** + * Reads from the file specified in the constructor, then converts the + * stored task strings into an array of {@link Task} objects using + * the {@link TaskParser} class. + *

+ * If the text stored in the file is not in the correct format, the line + * is skipped and its contents are not returned in the array. + *

+ * + * @return an array of {@link Task} objects + * @throws IOException if reading from the file is unsuccessful + */ + public Task[] readTasksFromFile() throws IOException { + if (!file.exists()) { + return new Task[0]; + } + + Scanner sc = new Scanner(new FileReader(file)); + sc.useDelimiter("\n"); + + ArrayList tasks = new ArrayList<>(); + while (sc.hasNext()) { + try { + String taskString = sc.nextLine(); + tasks.add(TaskParser.createTask(taskString, InputSource.FILE)); + } catch (IllegalArgumentException | DateTimeParseException e) { + // task contains faulty information in file, just skip the line + } + } + + sc.close(); + + // convert to task objects + return tasks.toArray(new Task[0]); + } + + /** + * Saves the specified tasks into the text file. + *

+ * This operation will overwrite the contents previously in the text file. + *

+ * + * @param tasks the tasks to save to the text file + * @throws IOException if writing to the file is unsuccessful + */ + public void saveTasksToFile(Task[] tasks) throws IOException { + createFileIfNotExists(); + + FileWriter writer = new FileWriter(file); + String result = convertToSaveFormat(tasks); + writer.write(result); + writer.close(); + } + + /** + * Converts and array of {@link Task} objects into a string. + * + * @param tasks the array of {@link Task} objects + * @return a string representing the array of {@link Task} objects + */ + private String convertToSaveFormat(Task[] tasks) { + StringBuilder result = new StringBuilder(); + for (Task task : tasks) { + result.append(task.toSaveFormat()); + result.append("\n"); + } + return result.toString(); + } + + /** + * Creates a file to store tasks if no such file already exists. + *

+ * The relevant parent directories are also created if needed. + *

+ * + * @throws IOException if the creation of the file was unsuccessful + */ + private void createFileIfNotExists() throws IOException { + if (file.exists()) { + return; + } + + boolean isParentDirectoryCreated = file.getParentFile().mkdirs(); + boolean isFileCreated = file.createNewFile(); + + if (!isParentDirectoryCreated) { + throw new IOException("An error occurred when trying to initialise the file directory " + file.getPath()); + } + + if (!isFileCreated) { + throw new IOException("An error occurred when trying to initialise " + file.getName()); + } + } +} diff --git a/src/main/java/lawrence/parser/CommandParser.java b/src/main/java/lawrence/parser/CommandParser.java new file mode 100644 index 0000000000..d3fe3b7c25 --- /dev/null +++ b/src/main/java/lawrence/parser/CommandParser.java @@ -0,0 +1,98 @@ +package lawrence.parser; + +import lawrence.command.AddTaskCommand; +import lawrence.command.Command; +import lawrence.command.CommandType; +import lawrence.command.CompleteTaskCommand; +import lawrence.command.DeleteTaskCommand; +import lawrence.command.DisplayTasksCommand; +import lawrence.command.ExitSessionCommand; +import lawrence.command.FindTasksCommand; +import lawrence.command.UncompleteTaskCommand; + +/** + * This class is used to make sense of text input by the user. + *

+ * The commands are translated from text into their relevant command object + * counterparts as specified in {@link CommandType}. + *

+ */ +public class CommandParser { + /** + * Converts the provided input string into a relevant {@link Command} object. + *

+ * The commands available are as specified in {@link CommandType} and are exhaustive. + * Any input that cannot be properly passed into a Command instance will result in + * an {@link IllegalArgumentException}. + *

+ * + * @param input the string containing a command to be parsed + * @return a {@link Command} object + * @throws IllegalArgumentException if the input cannot be parsed into a known command + * @throws IllegalStateException if the parsed {@link CommandType} is not recognised + */ + public static Command createCommand(String input) throws IllegalArgumentException, IllegalStateException { + if (input.isEmpty()) { + throw new IllegalArgumentException("Command input cannot be empty!"); + } + + String[] inputComponents = getInputComponents(input); + assert !inputComponents[0].isEmpty(); + CommandType type = determineCommandType(inputComponents[0]); + + Command c; + switch(type) { + case ADD_EVENT: + // fallthrough + case ADD_DEADLINE: + // fallthrough + case ADD_TODO: + c = new AddTaskCommand(type, input); + break; + case DELETE: + c = new DeleteTaskCommand(type, input); + break; + case DISPLAY: + c = new DisplayTasksCommand(type); + break; + case EXIT: + c = new ExitSessionCommand(type); + break; + case FIND_MATCHING: + c = new FindTasksCommand(type, input); + break; + case MARK_COMPLETE: + c = new CompleteTaskCommand(type, input); + break; + case MARK_INCOMPLETE: + c = new UncompleteTaskCommand(type, input); + break; + case INVALID: + throw new IllegalArgumentException(String.format("Unknown command: %s.", inputComponents[0])); + default: + throw new IllegalStateException(String.format("Unknown command type: %s", type)); + } + return c; + } + + /** + * Returns the components of a command from an input. + * + * @param input the full command input + * @return an array of length 2 containing the split input + */ + private static String[] getInputComponents(String input) { + return input.split(" ", 2); + } + + /** + * Returns a {@link CommandType} corresponding to the given input. + * + * @param input the input string containing information about a {@link CommandType} + * @return the {@link CommandType} corresponding to the input + * @throws IllegalArgumentException if input string does not match any command type + */ + private static CommandType determineCommandType(String input) throws IllegalArgumentException { + return CommandType.fromString(input); + } +} diff --git a/src/main/java/lawrence/parser/FileTaskCreator.java b/src/main/java/lawrence/parser/FileTaskCreator.java new file mode 100644 index 0000000000..4fd23a4b88 --- /dev/null +++ b/src/main/java/lawrence/parser/FileTaskCreator.java @@ -0,0 +1,141 @@ +package lawrence.parser; + +import java.time.LocalDateTime; + +import lawrence.task.Deadline; +import lawrence.task.Event; +import lawrence.task.Task; +import lawrence.task.TaskType; +import lawrence.task.Todo; +import lawrence.utils.DateParser; + +/** + * The concrete implementation of {@link TaskCreator} used to parse + * file input into a {@link Task} object. + */ +public class FileTaskCreator implements TaskCreator { + private static final int NUMBER_OF_DEADLINE_PARAMETERS = 3; + private static final int NUMBER_OF_EVENT_PARAMETERS = 4; + private static final int NUMBER_OF_TODO_PARAMETERS = 2; + + /** + * Converts a string input from a file containing task information + * into a {@link Task} object. + * + * @param input the string containing information about a task object + * @return a {@link Task} object + * @throws IllegalArgumentException if input is invalid + */ + @Override + public Task createTask(String input) { + if (input.isEmpty()) { + throw new IllegalArgumentException("Cannot create task from empty input!"); + } + + // separate the string containing information about the task type into index 0 + String[] inputComponents = input.split(" \\| ", 2); + + if (inputComponents.length != 2) { + throw new IllegalArgumentException("Unable to parse input from file"); + } + + // parse the type of task that needs to be created + TaskType type = TaskType.fromString(inputComponents[0]); + + Task t; + switch (type) { + case DEADLINE: + t = createDeadlineTask(inputComponents[1]); + break; + case EVENT: + t = createEventTask(inputComponents[1]); + break; + case TODO: + t = createTodoTask(inputComponents[1]); + break; + default: + throw new IllegalArgumentException("Unknown task type: " + type); + } + return t; + } + + /** + * Creates and returns a {@link Deadline} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Deadline} object + * @return a {@link Deadline} object + * @throws IllegalArgumentException if the input is invalid + * @see Deadline + */ + private Deadline createDeadlineTask(String input) throws IllegalArgumentException { + String[] parameters = input.split(" \\| ", NUMBER_OF_DEADLINE_PARAMETERS); + + if (parameters.length != NUMBER_OF_DEADLINE_PARAMETERS) { + throw new IllegalArgumentException( + String.format("Unable to parse deadline from file input. Expected %d parameters, got %d", + NUMBER_OF_DEADLINE_PARAMETERS, + parameters.length)); + } + + // deconstruct array elements into their respective attributes + boolean isComplete = parameters[0].equals("1"); + String description = parameters[1]; + LocalDateTime by = DateParser.parseFileInputDate(parameters[2]); + + return new Deadline(description, isComplete, by); + } + + /** + * Creates and returns an {@link Event} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Event} object + * @return an {@link Event} object + * @throws IllegalArgumentException if the input is invalid + * @see Event + */ + private Event createEventTask(String input) throws IllegalArgumentException { + String[] parameters = input.split(" \\| ", NUMBER_OF_EVENT_PARAMETERS); + + if (parameters.length != NUMBER_OF_EVENT_PARAMETERS) { + throw new IllegalArgumentException( + String.format("Unable to parse event from file input. Expected %d parameters, got %d", + NUMBER_OF_EVENT_PARAMETERS, + parameters.length)); + } + + // deconstruct array elements into their respective attributes + boolean isComplete = parameters[0].equals("1"); + String description = parameters[1]; + LocalDateTime from = DateParser.parseFileInputDate(parameters[2]); + LocalDateTime to = DateParser.parseFileInputDate(parameters[3]); + + return new Event(description, isComplete, from, to); + } + + /** + * Creates and returns a {@link Todo} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Todo} object + * @return a {@link Todo} object + * @see Todo + */ + private Todo createTodoTask(String input) { + String[] parameters = input.split(" \\| ", NUMBER_OF_TODO_PARAMETERS); + + if (parameters.length != NUMBER_OF_TODO_PARAMETERS) { + throw new IllegalArgumentException( + String.format("Unable to parse todo from file input. Expected %d parameters, got %d", + NUMBER_OF_TODO_PARAMETERS, + parameters.length)); + } + + // deconstruct array elements into their respective attributes + boolean isComplete = parameters[0].equals("1"); + String description = parameters[1]; + + return new Todo(description, isComplete); + } +} diff --git a/src/main/java/lawrence/parser/InputSource.java b/src/main/java/lawrence/parser/InputSource.java new file mode 100644 index 0000000000..ce21393d11 --- /dev/null +++ b/src/main/java/lawrence/parser/InputSource.java @@ -0,0 +1,9 @@ +package lawrence.parser; + +/** + * Represents the different sources that a string input can come from. + */ +public enum InputSource { + FILE, + USER +} diff --git a/src/main/java/lawrence/parser/TaskCreator.java b/src/main/java/lawrence/parser/TaskCreator.java new file mode 100644 index 0000000000..3722990be8 --- /dev/null +++ b/src/main/java/lawrence/parser/TaskCreator.java @@ -0,0 +1,17 @@ +package lawrence.parser; + +import lawrence.task.Task; + +/** + * Represents an object that can receive a string as input + * and convert it into a relevant {@link Task} object. + */ +public interface TaskCreator { + /** + * Converts the given input into a {@link Task} object. + * + * @param input the string containing information about a task object + * @return a {@link Task} object + */ + Task createTask(String input); +} diff --git a/src/main/java/lawrence/parser/TaskParser.java b/src/main/java/lawrence/parser/TaskParser.java new file mode 100644 index 0000000000..c6dff88492 --- /dev/null +++ b/src/main/java/lawrence/parser/TaskParser.java @@ -0,0 +1,47 @@ +package lawrence.parser; + +import lawrence.task.Task; + +/** + * This class is used to make sense of text read from a file. + *

+ * The commands are translated from text into their relevant command object + * counterparts as specified in {@link lawrence.task.TaskType}. + *

+ */ +public class TaskParser { + /** + * Converts the provided input string into a relevant {@link Task} object + * based on the {@link InputSource} specified. + *

+ * The input sources available are as specified in {@link InputSource} and are exhaustive. + * If the input source is unrecognised, an {@link IllegalStateException} is thrown. + *

+ * + * @param input the string containing task information to be parsed + * @return a {@link Task} object + * @throws IllegalArgumentException if the input is invalid + * @throws IllegalStateException if the {@link InputSource} is not recognised + */ + public static Task createTask(String input, InputSource source) + throws IllegalArgumentException, IllegalStateException { + + if (input.isEmpty()) { + throw new IllegalArgumentException("Task input cannot be empty!"); + } + + TaskCreator creator; + switch(source) { + case FILE: + creator = new FileTaskCreator(); + break; + case USER: + creator = new UserTaskCreator(); + break; + default: + // this case should never be reached + throw new IllegalStateException("Unexpected source: " + source); + } + return creator.createTask(input); + } +} diff --git a/src/main/java/lawrence/parser/UserTaskCreator.java b/src/main/java/lawrence/parser/UserTaskCreator.java new file mode 100644 index 0000000000..39defcf0d0 --- /dev/null +++ b/src/main/java/lawrence/parser/UserTaskCreator.java @@ -0,0 +1,126 @@ +package lawrence.parser; + +import java.time.LocalDateTime; + +import lawrence.task.Deadline; +import lawrence.task.Event; +import lawrence.task.Task; +import lawrence.task.TaskType; +import lawrence.task.Todo; +import lawrence.utils.DateParser; + +/** + * The concrete implementation of {@link TaskCreator} used to parse + * user input into a {@link Task} object. + */ +public class UserTaskCreator implements TaskCreator { + private static final int NUMBER_OF_DEADLINE_PARAMETERS = 2; + private static final int NUMBER_OF_EVENT_PARAMETERS = 3; + + /** + * Converts a string input by the user containing task information + * into a {@link Task} object. + * + * @param input the string containing information about a task object + * @return a {@link Task} object + * @throws IllegalArgumentException if input is invalid + */ + @Override + public Task createTask(String input) throws IllegalArgumentException { + if (input.isEmpty()) { + throw new IllegalArgumentException("Cannot create task from empty input!"); + } + + // separates the string containing information about the task type into index 0 + String[] inputComponents = input.split(" ", 2); + + if (inputComponents.length != 2) { + throw new IllegalArgumentException("Task information cannot be empty."); + } + + // parse the type of task that needs to be created + TaskType type = TaskType.fromString(inputComponents[0]); + + Task t; + switch (type) { + case DEADLINE: + t = createDeadlineTask(inputComponents[1]); + break; + case EVENT: + t = createEventTask(inputComponents[1]); + break; + case TODO: + t = createTodoTask(inputComponents[1]); + break; + default: + throw new IllegalArgumentException("Unknown task type: " + type); + } + return t; + } + + /** + * Creates and returns a {@link Deadline} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Deadline} object + * @return a {@link Deadline} object + * @throws IllegalArgumentException if the input is invalid + * @see Deadline + */ + private Deadline createDeadlineTask(String input) throws IllegalArgumentException { + String[] parameters = input.split(" /by ", NUMBER_OF_DEADLINE_PARAMETERS); + + if (parameters.length != NUMBER_OF_DEADLINE_PARAMETERS) { + throw new IllegalArgumentException( + String.format("Unable to create deadline from user input. Expected %d parameters, got %d", + NUMBER_OF_DEADLINE_PARAMETERS, + parameters.length)); + } + + // deconstruct array elements into their respective attributes + String description = parameters[0]; + LocalDateTime by = DateParser.parseUserInputDate(parameters[1]); + + return new Deadline(description, by); + } + + /** + * Creates and returns an {@link Event} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Event} object + * @return an {@link Event} object + * @throws IllegalArgumentException if the input is invalid + * @see Event + */ + private Event createEventTask(String input) { + String[] parameters = input.split(" /from | /to ", NUMBER_OF_EVENT_PARAMETERS); + if (parameters.length != NUMBER_OF_EVENT_PARAMETERS) { + throw new IllegalArgumentException( + String.format("Unable to create event from user input. Expected %d parameters, got %d", + NUMBER_OF_EVENT_PARAMETERS, + parameters.length)); + } + + assert parameters.length == NUMBER_OF_EVENT_PARAMETERS; + + // deconstruct array elements into their respective attributes + String description = parameters[0]; + LocalDateTime from = DateParser.parseUserInputDate(parameters[1]); + LocalDateTime to = DateParser.parseUserInputDate(parameters[2]); + + return new Event(description, from, to); + } + + /** + * Creates and returns a {@link Todo} object based on the + * input information provided. + * + * @param input a string containing information about the {@link Todo} object + * @return a {@link Todo} object + * @see Todo + */ + private Todo createTodoTask(String input) { + return new Todo(input); + } +} diff --git a/src/main/java/lawrence/task/Deadline.java b/src/main/java/lawrence/task/Deadline.java new file mode 100644 index 0000000000..709ff385a2 --- /dev/null +++ b/src/main/java/lawrence/task/Deadline.java @@ -0,0 +1,67 @@ +package lawrence.task; + +import java.time.LocalDateTime; + +import lawrence.utils.DateParser; + +/** + * Represents a task with a deadline. + */ +public class Deadline extends Task { + private final LocalDateTime by; + + /** + * Constructor. Creates a {@link Deadline} object with the specified + * task description and completion date. + *

+ * The task will be marked as incomplete by default. + *

+ * + * @param description the name of the deadline + * @param by the date to complete the deadline by + */ + public Deadline(String description, LocalDateTime by) { + super(description); + this.by = by; + } + + /** + * Constructor. Creates a {@link Deadline} object with the specified + * task description, completion status and completion date. + *

+ * The task can be marked as complete or incomplete on creation. + *

+ * + * @param description the name of the deadline + * @param isComplete indicates whether the deadline is complete + * @param by the date by which the deadline should be completed + */ + public Deadline(String description, boolean isComplete, LocalDateTime by) { + super(description, isComplete); + this.by = by; + } + + /** + * Returns a string representation of the object in a + * standardised save format. + * + * @return a string representation of the object in save format + */ + public String toSaveFormat() { + return String.format("D | %s | %s", + super.toSaveFormat(), + DateParser.toOutputString(by)); + } + + /** + * Returns a string representation of the object. + * + * @return a string representation of the object + */ + @Override + public String toString() { + return String.format("[D]%s (by: %s)", + super.toString(), + DateParser.toOutputString(by)); + } +} diff --git a/src/main/java/lawrence/task/Event.java b/src/main/java/lawrence/task/Event.java new file mode 100644 index 0000000000..123e7ef367 --- /dev/null +++ b/src/main/java/lawrence/task/Event.java @@ -0,0 +1,73 @@ +package lawrence.task; + +import java.time.LocalDateTime; + +import lawrence.utils.DateParser; + +/** + * Represents a task that has a start time and an end time. + */ +public class Event extends Task { + private final LocalDateTime from; + private final LocalDateTime to; + + /** + * Constructor. Creates an {@link Event} object with the specified + * task description, start time and end time. + *

+ * The task will be marked as incomplete by default. + *

+ * + * @param description the name of the event + * @param from the start date of the event + * @param to the end date of the event + */ + public Event(String description, LocalDateTime from, LocalDateTime to) { + super(description); + this.from = from; + this.to = to; + } + + /** + * Constructor. Creates an {@link Event} object with the specified + * task description, completion status, start time and end time. + *

+ * The task can be marked as complete or incomplete on creation. + *

+ * + * @param description the name of the event + * @param isComplete indicates whether the event is complete + * @param from the start date of the event + * @param to the end date of the event + */ + public Event(String description, boolean isComplete, LocalDateTime from, LocalDateTime to) { + super(description, isComplete); + this.from = from; + this.to = to; + } + + /** + * Returns a string representation of the object in a + * standardised save format. + * + * @return a string representation of the object in save format + */ + public String toSaveFormat() { + return String.format("E | %s | %s | %s", + super.toSaveFormat(), + DateParser.toOutputString(from), + DateParser.toOutputString(to)); + } + + /** + * Returns a string representation of the object. + * + * @return a string representation of the object + */ + @Override + public String toString() { + return String.format("[E]%s (from: %s to: %s)", super.toString(), + DateParser.toOutputString(from), + DateParser.toOutputString(to)); + } +} diff --git a/src/main/java/lawrence/task/Task.java b/src/main/java/lawrence/task/Task.java new file mode 100644 index 0000000000..0140268d5c --- /dev/null +++ b/src/main/java/lawrence/task/Task.java @@ -0,0 +1,77 @@ +package lawrence.task; + +/** + * Represents a real-life task that needs to be completed. + */ +public abstract class Task { + private boolean isComplete; + private final String description; + + /** + * Constructor. Creates a {@link Task} object with the specified + * task description. + *

+ * The task will be marked as incomplete by default. + *

+ * @param description the name of the task + */ + public Task(String description) { + this.description = description; + isComplete = false; + } + + /** + * Constructor. Creates a {@link Task} object with the specified + * task description and completion status. + *

+ * The task can be marked as complete or incomplete on creation. + *

+ * + * @param description the name of the task + * @param isComplete indicates whether the task is complete + */ + public Task(String description, boolean isComplete) { + this.description = description; + this.isComplete = isComplete; + } + + /** + * Returns a boolean indicate if the query matches or partially matches + * the task description. + * + * @param query the string to match with the description + * @return true if there is a match or partial match, false otherwise + */ + public boolean contains(String query) { + return description.contains(query); + } + + /** + * Sets the completion status of the task to the specified value. + * + * @param isComplete indicates whether the task is complete + */ + public void setComplete(boolean isComplete) { + this.isComplete = isComplete; + } + + /** + * Returns a string representation of the object in a + * standardised save format. + * + * @return a string representation of the object in save format + */ + public String toSaveFormat() { + return String.format("%s | %s", isComplete ? "1" : "0", description); + } + + /** + * Returns a string representation of the object. + * + * @return a string representation of the object + */ + @Override + public String toString() { + return String.format("[%s] %s", isComplete ? "X" : " ", description); + } +} diff --git a/src/main/java/lawrence/task/TaskList.java b/src/main/java/lawrence/task/TaskList.java new file mode 100644 index 0000000000..f3e6292e9c --- /dev/null +++ b/src/main/java/lawrence/task/TaskList.java @@ -0,0 +1,169 @@ +package lawrence.task; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the list of tasks the user has specified to be tracked by the chatbot. + */ +public class TaskList { + private final ArrayList tasks; + + /** + * Default constructor. The number of items in the list will be + * initialised to 0 by default. + */ + public TaskList() { + tasks = new ArrayList<>(); + } + + /** + * Constructor. The items in the list will be initialised to the specified + * {@link Task} objects provided in the argument. + * + * @param tasks the array containing the {@link Task} objects to initialise + * the list with + */ + public TaskList(Task[] tasks) { + this.tasks = new ArrayList<>(List.of(tasks)); + } + + /** + * Adds a new task to the list. + * + * @param task the {@link Task} object to be added + */ + public void addTask(Task task) { + tasks.add(task); + } + + /** + * Removes a task from the list based on the number provided. + *

+ * Task numbers start from one. + *

+ * + * @param taskNumber the index of the task to delete. Starts from 1 + * @return the {@link Task} object removed from the list + * @throws IllegalArgumentException if the task number provided is out of bounds + * @throws IllegalStateException if there are no tasks in the list + */ + public Task deleteTask(int taskNumber) throws IllegalArgumentException, IllegalStateException { + if (tasks.isEmpty()) { + throw new IllegalStateException("There are no tasks that can be chosen for deletion."); + } + + if (taskNumber < 1 || taskNumber > tasks.size()) { + throw new IllegalArgumentException( + String.format("Task does not exist. Number must be within the range 1 to %s.", tasks.size())); + } + + return tasks.remove(taskNumber - 1); + } + + /** + * Marks a task from the list as complete based on the number provided. + *

+ * Task numbers start from one. + *

+ * + * @param taskNumber the index of the task to delete. Starts from 1 + * @return the {@link Task} object that was updated in the operation + * @throws IllegalArgumentException if the task number provided is out of bounds + * @throws IllegalStateException if there are no tasks in the list + */ + public Task completeTask(int taskNumber) { + if (tasks.isEmpty()) { + throw new IllegalStateException("There are no tasks that can be chosen to be marked as complete."); + } + + if (taskNumber < 1 || taskNumber > tasks.size()) { + throw new IllegalArgumentException( + String.format("Task does not exist. Number must be within the range 1 to %s.", tasks.size())); + } + + Task t = tasks.get(taskNumber - 1); + assert t != null; + t.setComplete(true); + return t; + } + + /** + * Marks a task from the list as incomplete based on the number provided. + *

+ * Task numbers start from one. + *

+ * + * @param taskNumber the index of the task to delete. Starts from 1 + * @return the {@link Task} object that was updated in the operation + * @throws IllegalArgumentException if the task number provided is out of bounds + * @throws IllegalStateException if there are no tasks in the list + */ + public Task uncompleteTask(int taskNumber) { + if (tasks.isEmpty()) { + throw new IllegalStateException("There are no tasks that can be chosen to be marked as incomplete."); + } + + if (taskNumber < 1 || taskNumber > tasks.size()) { + throw new IllegalArgumentException( + String.format("Task does not exist. Number must be within the range 1 to %s.", tasks.size())); + } + + Task t = tasks.get(taskNumber - 1); + t.setComplete(false); + return t; + } + + /** + * Matches the specified query with tasks descriptions and returns a + * {@link TaskList} containing tasks which match the query. + * + * @param query the string to match task descriptions + * @return a {@link TaskList} containing hits + */ + public TaskList findTasks(String query) { + List result = tasks.stream() + .filter(task -> task.contains(query)) + .toList(); + + return new TaskList(result.toArray(new Task[0])); + } + + /** + * Returns an array of {@link Task} objects stored in the list. + * + * @return an array of {@link Task} objects + */ + public Task[] getTasks() { + return tasks.toArray(new Task[0]); + } + + /** + * Returns the size of the current list. + * + * @return the size of the current list + */ + public int getSize() { + return tasks.size(); + } + + /** + * Returns a string representation of the list. + * + * @return a string representation of the list + */ + @Override + public String toString() { + if (tasks.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < tasks.size(); i++) { + result.append(String.format("%d.%s%n", i + 1, tasks.get(i))); + } + + // exclude the last newline character from getting printed + return result.substring(0, result.length() - 2); + } +} diff --git a/src/main/java/lawrence/task/TaskType.java b/src/main/java/lawrence/task/TaskType.java new file mode 100644 index 0000000000..4f1c8527d5 --- /dev/null +++ b/src/main/java/lawrence/task/TaskType.java @@ -0,0 +1,56 @@ +package lawrence.task; + +/** + * Represents the different tasks that can be handled by the chatbot. + */ +public enum TaskType { + DEADLINE("deadline"), + EVENT("event"), + TODO("todo"); + + private final String taskType; + + /** + * Default constructor. + *

+ * The input string is converted into lowercase for greater input flexibility. + *

+ * + * @param type the string containing an enum value + */ + TaskType(String type) { + this.taskType = type.toLowerCase(); + } + + /** + * Converts a text string into its enum counterpart. + * + * @param input the input containing an enum value + * @return a TaskType enum if it exists + * @throws IllegalArgumentException if string does not match any enum values + */ + public static TaskType fromString(String input) throws IllegalArgumentException { + for (TaskType task : TaskType.values()) { + String taskType = task.getTaskType(); + // Check for exact match + if (taskType.equalsIgnoreCase(input)) { + return task; + } + + // Check for match by first letter if no exact match found + if (taskType.startsWith(String.valueOf(input.charAt(0)).toLowerCase())) { + return task; + } + } + throw new IllegalArgumentException("No task type found for input: " + input); + } + + /** + * Returns the task type as a string. + * + * @return the task type as a string + */ + public String getTaskType() { + return taskType; + } +} diff --git a/src/main/java/lawrence/task/Todo.java b/src/main/java/lawrence/task/Todo.java new file mode 100644 index 0000000000..251779b633 --- /dev/null +++ b/src/main/java/lawrence/task/Todo.java @@ -0,0 +1,53 @@ +package lawrence.task; + +/** + * Represents a simple task with no time constraints. + */ +public class Todo extends Task { + /** + * Constructor. Creates an {@link Todo} object with the specified + * task description. + *

+ * The task will be marked as incomplete by default. + *

+ * + * @param description the name of the task + */ + public Todo(String description) { + super(description); + } + + /** + * Constructor. Creates an {@link Todo} object with the specified + * task description and completion status. + *

+ * The task can be marked as complete or incomplete on creation. + *

+ * + * @param description the name of the task + * @param isComplete indicates whether the task is complete + */ + public Todo(String description, boolean isComplete) { + super(description, isComplete); + } + + /** + * Returns a string representation of the object in a + * standardised save format. + * + * @return a string representation of the object in save format + */ + public String toSaveFormat() { + return String.format("T | %s", super.toSaveFormat()); + } + + /** + * Returns a string representation of the object. + * + * @return a string representation of the object + */ + @Override + public String toString() { + return String.format("[T]%s", super.toString()); + } +} diff --git a/src/main/java/lawrence/ui/Launcher.java b/src/main/java/lawrence/ui/Launcher.java new file mode 100644 index 0000000000..9812798655 --- /dev/null +++ b/src/main/java/lawrence/ui/Launcher.java @@ -0,0 +1,12 @@ +package lawrence.ui; + +import javafx.application.Application; + +/** + * A launcher class to work around classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/lawrence/ui/Main.java b/src/main/java/lawrence/ui/Main.java new file mode 100644 index 0000000000..0cf6c7ebf6 --- /dev/null +++ b/src/main/java/lawrence/ui/Main.java @@ -0,0 +1,36 @@ +package lawrence.ui; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; +import lawrence.app.Lawrence; +import lawrence.ui.components.MainWindow; + +/** + * A GUI for Lawrence using FXML. + */ +public class Main extends Application { + private final Lawrence lawrence = new Lawrence(); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + + Scene scene = new Scene(ap); + stage.setScene(scene); + stage.setTitle("Lawrence Chatbot"); + + fxmlLoader.getController().setLawrence(lawrence); + fxmlLoader.getController().showWelcomeMessage(); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/lawrence/ui/UserInterface.java b/src/main/java/lawrence/ui/UserInterface.java new file mode 100644 index 0000000000..61525db6c1 --- /dev/null +++ b/src/main/java/lawrence/ui/UserInterface.java @@ -0,0 +1,53 @@ +package lawrence.ui; + +/** + * Represents the interface where messages can be displayed to the user in the console. + */ +public class UserInterface { + private final String name; + + /** + * Constructor. The name of the bot can be specified for + * the bot to identify itself to the user. + * + * @param name the name of the chatbot + */ + public UserInterface(String name) { + this.name = name; + } + + /** + * Displays a message greeting the user. + */ + public void greetUser() { + String greeting = String.format("Hello! I'm %s and I'm here to establish another GST hike.%n" + + "What can I do for you?", name); + showMessage(greeting); + } + + /** + * Displays an exit message. + */ + public void showExitMessage() { + showMessage("That's all folks! Hope to see you again soon!"); + } + + /** + * Shows the specified message in the console. + * Messages will be separated from each other using horizontal lines. + * + * @param message the message to display + */ + public void showMessage(String message) { + showHorizontalLine(); + System.out.println(message); + showHorizontalLine(); + } + + /** + * Displays a horizontal line in the console. + */ + public void showHorizontalLine() { + System.out.println("===================="); + } +} diff --git a/src/main/java/lawrence/ui/components/DialogBox.java b/src/main/java/lawrence/ui/components/DialogBox.java new file mode 100644 index 0000000000..fe210a6d0c --- /dev/null +++ b/src/main/java/lawrence/ui/components/DialogBox.java @@ -0,0 +1,113 @@ +package lawrence.ui.components; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import lawrence.app.Response; +import lawrence.command.CommandType; + +/** + * Represents a dialog box consisting of an ImageView to represent the speaker's face + * and a label containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + /** + * Constructor. + * + * @param text the text to be displayed in the dialog box + * @param image the image to be displayed in the dialog box + */ + private DialogBox(String text, Image image) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(image); + } + + /** + * Returns a dialog box originating from the user. + * + * @param text the text to be displayed in the dialog box + * @param image the image associated with the user + * @return a dialog box for the user + */ + public static DialogBox getUserDialog(String text, Image image) { + return new DialogBox(text, image); + } + + /** + * Returns a dialog box originating from the bot. + * + * @param response a {@link Response} object from the bot + * @param image the image associated with the bot + * @return a dialog box for the bot + */ + public static DialogBox getBotDialog(Response response, Image image) { + var db = new DialogBox(response.message(), image); + db.flip(); + db.changeDialogStyle(response.commandType()); + return db; + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + dialog.getStyleClass().add("reply-label"); + } + + /** + * Sets the style of the dialog box depending on the type of command issued. + * + * @param type the type of command issued to the bot + */ + private void changeDialogStyle(CommandType type) { + switch(type) { + case ADD_EVENT: + // Fallthrough + case ADD_DEADLINE: + // Fallthrough + case ADD_TODO: + dialog.getStyleClass().add("add-label"); + break; + case DELETE: + dialog.getStyleClass().add("delete-label"); + break; + case MARK_COMPLETE: + // Fallthrough + case MARK_INCOMPLETE: + dialog.getStyleClass().add("marked-label"); + break; + case INVALID: + // Fallthrough + default: + // Do nothing + } + } +} diff --git a/src/main/java/lawrence/ui/components/MainWindow.java b/src/main/java/lawrence/ui/components/MainWindow.java new file mode 100644 index 0000000000..5854591c06 --- /dev/null +++ b/src/main/java/lawrence/ui/components/MainWindow.java @@ -0,0 +1,108 @@ +package lawrence.ui.components; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import lawrence.app.Lawrence; +import lawrence.app.Response; +import lawrence.command.CommandType; + +/** + * Controller for the main GUI. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Lawrence lawrence; + + private final Image userImage = new Image(this.getClass().getResourceAsStream("/images/baller.jpg")); + private final Image botImage = new Image(this.getClass().getResourceAsStream("/images/lawrence.jpg")); + + /** + * Initialises the child components in the main window. + */ + @FXML + public void initialize() { + setScrollListener(); + } + + /** + * Injects the Bot instance. + */ + public void setLawrence(Lawrence lawrence) { + this.lawrence = lawrence; + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing the bot reply and then + * appends them to the dialog container. + *

+ * Clears the user input field after processing. + *

+ */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + Response response = lawrence.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getBotDialog(response, botImage) + ); + + if (!response.shouldContinue()) { + Platform.exit(); + } + + userInput.clear(); + scrollToBottom(); + } + + /** + * Displays a welcome message. + */ + public void showWelcomeMessage() { + Response welcomeResponse = new Response(CommandType.INVALID, + lawrence.getWelcomeMessage(), + true); + dialogContainer.getChildren().add( + DialogBox.getBotDialog(welcomeResponse, botImage)); + } + + /** + * Initialises a listener to handle scroll events. + */ + private void setScrollListener() { + dialogContainer.setOnScroll(event -> { + // get change in scroll direction + double deltaY = event.getDeltaY(); + double scrollAmount = scrollPane.getVvalue() - deltaY + / scrollPane.getContent().getBoundsInLocal().getHeight(); + + scrollPane.setVvalue(scrollAmount); + }); + } + + /** + * Sets the vertical value of the scroll pane to emulate scrolling to the bottom of the dialog box. + */ + private void scrollToBottom() { + // ensure layout is updated before scrolling + dialogContainer.layout(); + scrollPane.layout(); + + // scroll to the bottom + scrollPane.setVvalue(1.0); + } +} diff --git a/src/main/java/lawrence/utils/DateParser.java b/src/main/java/lawrence/utils/DateParser.java new file mode 100644 index 0000000000..433dc51c6b --- /dev/null +++ b/src/main/java/lawrence/utils/DateParser.java @@ -0,0 +1,49 @@ +package lawrence.utils; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * A utility class used to standardise how dates are stored and displayed. + */ +public class DateParser { + public static final String FORMAT_STRING_FOR_USER_INPUT = "yyyy-MM-dd HH:mm"; + public static final String FORMAT_STRING_FOR_STORAGE = "dd-MM-yyyy HH:mm"; + private static final DateTimeFormatter FORMATTER_FOR_USER_INPUT = DateTimeFormatter + .ofPattern(FORMAT_STRING_FOR_USER_INPUT); + private static final DateTimeFormatter FORMATTER_FOR_STORAGE = DateTimeFormatter + .ofPattern(FORMAT_STRING_FOR_STORAGE); + + /** + * Converts user input into a {@link LocalDateTime} object. + * Formatting is based on {@link #FORMAT_STRING_FOR_USER_INPUT}. + * + * @param input the string to convert to a {@link LocalDateTime} object + * @return a {@link LocalDateTime} object + */ + public static LocalDateTime parseUserInputDate(String input) { + return LocalDateTime.parse(input, FORMATTER_FOR_USER_INPUT); + } + + /** + * Converts file input into a {@link LocalDateTime} object. + * Formatting is based on {@link #FORMAT_STRING_FOR_STORAGE}. + * + * @param input the string to convert to a {@link LocalDateTime} object + * @return a {@link LocalDateTime} object + */ + public static LocalDateTime parseFileInputDate(String input) { + return LocalDateTime.parse(input, FORMATTER_FOR_STORAGE); + } + + /** + * Converts a {@link LocalDateTime} object into a string. + * Formatting is based on {@link #FORMAT_STRING_FOR_STORAGE}. + * + * @param input the {@link LocalDateTime} object to be converted to a string + * @return a string representing the {@link LocalDateTime} object + */ + public static String toOutputString(LocalDateTime input) { + return input.format(FORMATTER_FOR_STORAGE); + } +} diff --git a/src/main/resources/css/dialog-box.css b/src/main/resources/css/dialog-box.css new file mode 100644 index 0000000000..819c665b1f --- /dev/null +++ b/src/main/resources/css/dialog-box.css @@ -0,0 +1,38 @@ +.label { + -fx-background-color: #1fc600; + -fx-border-color: #089000; + -fx-border-width: 2px; + -fx-background-radius: 1em 1em 0 1em; + -fx-border-radius: 1em 1em 0 1em; +} + +.reply-label { + -fx-background-color: #CCCCCC; + -fx-border-color: #B3B3B3; + -fx-background-radius: 1em 1em 1em 0; + -fx-border-radius: 1em 1em 1em 0; +} + +.add-label { + -fx-background-color: yellow; +} + +.marked-label { + -fx-background-color: palegreen; +} + +.delete-label { + -fx-background-color: lightpink; +} + +#displayPicture { + /* Shadow effect on image. */ + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 10, 0.5, 5, 5); + + /* Change size of image. */ + -fx-scale-x: 1; + -fx-scale-y: 1; + + /* Rotate image clockwise by degrees. */ + -fx-rotate: 0; +} \ No newline at end of file diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css new file mode 100644 index 0000000000..07c1e63758 --- /dev/null +++ b/src/main/resources/css/main.css @@ -0,0 +1,51 @@ +.root { + main-color: rgb(237, 255, 242); /* Create a looked-up color called "main-color" within root. */ + -fx-background-color: main-color; + -fx-background-image: url("../images/background.jpg"); /* Set image */ + -fx-background-size: cover; +} + +.text-field { + -fx-background-color: #d9ffe2; + -fx-font: 20px "Arial"; +} + +.button { + -fx-background-color: mediumspringgreen; + -fx-font: italic bold 16px "Arial"; +} + +.button:hover { + -fx-background-color:cyan; + -fx-font-size: 18px; +} + +.button:pressed { + -fx-background-color:orange; + -fx-font-size: 20px; +} + +.scroll-pane { + -fx-background-color: rgba(0,0,0,.5); +} + +.scroll-pane .viewport { + -fx-background-color: transparent; +} + +.scroll-bar { + -fx-font-size: 10px; /* Change width of scroll bar. */ + -fx-background-color: main-color; +} + +.scroll-bar .thumb { + -fx-background-color: #808080; + -fx-background-radius: 1em; +} + +/* Hides the increment and decrement buttons. */ +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-pref-height: 0; + -fx-opacity: 0; +} \ No newline at end of file diff --git a/src/main/resources/images/background.jpg b/src/main/resources/images/background.jpg new file mode 100644 index 0000000000..acfbe93e5a Binary files /dev/null and b/src/main/resources/images/background.jpg differ diff --git a/src/main/resources/images/baller.jpg b/src/main/resources/images/baller.jpg new file mode 100644 index 0000000000..50ae877878 Binary files /dev/null and b/src/main/resources/images/baller.jpg differ diff --git a/src/main/resources/images/lawrence.jpg b/src/main/resources/images/lawrence.jpg new file mode 100644 index 0000000000..df099a5054 Binary files /dev/null and b/src/main/resources/images/lawrence.jpg differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..821bd6e218 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..cf32564ae9 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + +