diff --git a/.classpath b/.classpath deleted file mode 100644 index 3f05f311a90b..000000000000 --- a/.classpath +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/.gitignore b/.gitignore index 45a20de82e87..823d175eb670 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ build/ .gradle/ +.checkstyle +.settings/ .idea/ lib/* *.iml @@ -14,3 +16,5 @@ preferences.json classes/ /data/ /bin/ +src/main/resources/docs/ +out/ diff --git a/.project b/.project deleted file mode 100644 index 1c9339c5f927..000000000000 --- a/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - addressbook-level4 - Project addressbook-level4 created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.buildship.core.gradleprojectnature - org.eclipse.jdt.core.javanature - - diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index 4e0fc71ac89f..000000000000 --- a/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,11 +0,0 @@ -build.commands=org.eclipse.jdt.core.javabuilder -connection.arguments= -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.gradle.user.home=null -connection.java.home=null -connection.jvm.arguments= -connection.project.dir= -derived.resources=.gradle,build -eclipse.preferences.version=1 -natures=org.eclipse.jdt.core.javanature -project.path=\: diff --git a/.travis.yml b/.travis.yml index a9d9e9b47d87..4a41176953e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,28 @@ language: java matrix: include: - jdk: oraclejdk8 -script: travis_retry ./gradlew clean headless allTests coverage coveralls -i -before_install: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" + +script: >- + ./config/travis/run-checks.sh && + travis_retry ./gradlew clean checkstyleMain checkstyleTest headless allTests coverage coveralls asciidoctor + +deploy: + skip_cleanup: true + provider: script + script: ./config/travis/deploy_github_pages.sh + on: + branch: master addons: apt: packages: - oracle-java8-installer + +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ diff --git a/README.adoc b/README.adoc new file mode 100644 index 000000000000..b2f955fef6b4 --- /dev/null +++ b/README.adoc @@ -0,0 +1,38 @@ += LISA: Levels Integrated Secretarial Agent +ifdef::env-github,env-browser[:relfileprefix: docs/] +ifdef::env-github,env-browser[:outfilesuffix: .adoc] + +https://travis-ci.org/CS2103AUG2017-T13-B1/main[image:https://travis-ci.org/CS2103AUG2017-T13-B1/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] +https://coveralls.io/github/CS2103AUG2017-T13-B1/main?branch=master/badge.svg?branch=master[image:https://coveralls.io/repos/github/CS2103AUG2017-T13-B1/main/badge.svg?branch=master[Coverage Status]] + +ifdef::env-github[] +image::docs/images/Ui.png[width="600"] +endif::[] + +ifndef::env-github[] +image::images/Ui.png[width="600"] +endif::[] + +* LISA is a desktop contacts managerial application. She has a GUI but most user interactions happen using a CLI (Command Line Interface). +* She is a intended to assist Insurance Agents in managing their contacts as well as insurance policies. +* She is *written in OOP fashion* with around 6 KLoC, adapted and improved from [https://github.com/se-edu/addressbook-level4[Addressbook-level4]]. +* New and Added features: +** Improved Tag management. +** Greater flexibility in creating a contact (Only a name is required). +** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. + +== Site Map + +* <> +* <> +* <> +* <> +* <> + +== Acknowledgements + +* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by +_Marco Jakob_. + +== Licence : link:LICENSE[MIT] diff --git a/README.md b/README.md deleted file mode 100644 index 249a00b3899c..000000000000 --- a/README.md +++ /dev/null @@ -1,34 +0,0 @@ -[![Build Status](https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master)](https://travis-ci.org/se-edu/addressbook-level4) -[![Coverage Status](https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master)](https://coveralls.io/github/se-edu/addressbook-level4?branch=master) - -# Address Book (Level 4) - -
- -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using - a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as - the main programming language. -* It is **written in OOP fashion**. It provides a **reasonably well-written** code example that is - **significantly bigger** (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from [level 3](https://github.com/se-edu/addressbook-level3): - * A more sophisticated GUI that includes a list panel and an in-built Browser. - * More test cases, including automated GUI testing. - * Support for *Build Automation* using Gradle and for *Continuous Integration* using Travis CI. - - -#### Site Map -* [User Guide](docs/UserGuide.md) -* [Developer Guide](docs/DeveloperGuide.md) -* [Learning Outcomes](docs/LearningOutcomes.md) -* [About Us](docs/AboutUs.md) -* [Contact Us](docs/ContactUs.md) - - -#### Acknowledgements - -* Some parts of this sample application were inspired by the excellent - [Java FX tutorial](http://code.makery.ch/library/javafx-8-tutorial/) by *Marco Jakob*. - - -#### Licence : [MIT](LICENSE) diff --git a/_config.yml b/_config.yml new file mode 100644 index 000000000000..277f1f2c510d --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000000..4660f313ab8b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,18 @@ +# AppVeyor configuration file +# For more details see https://www.appveyor.com/docs/build-configuration/ + +# Call on gradle to build and run tests +# --no-daemon: Prevent the daemon from launching to prevent file-in-use errors +# when we cache the ~/.gradle directory +build_script: + - gradlew.bat --no-daemon assemble checkstyleMain checkstyleTest + +test_script: + - appveyor-retry gradlew.bat --no-daemon headless allTests + +environment: + JAVA_HOME: C:\Program Files\Java\jdk1.8.0 # Use 64-bit Java + +# Files/folders to preserve between builds to speed them up +cache: + - C:\Users\appveyor\.gradle diff --git a/build.gradle b/build.gradle index 46b06c1e42ec..64c686e8f056 100644 --- a/build.gradle +++ b/build.gradle @@ -4,16 +4,23 @@ * For more details take a look at the Java Quickstart chapter in the Gradle * user guide available at http://gradle.org/docs/2.2.1/userguide/tutorial_java_projects.html */ - + plugins { id "com.github.kt3k.coveralls" version "2.4.0" id "com.github.johnrengelman.shadow" version '1.2.3' + id 'org.asciidoctor.convert' version '1.5.3' + id 'application' } +// Specifies the entry point of the application +mainClassName = 'seedu.address.MainApp' + allprojects { apply plugin: 'idea' apply plugin: 'java' apply plugin: 'jacoco' + apply plugin: 'checkstyle' + sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -33,12 +40,17 @@ allprojects { jacksonVersion = '2.7.0' jacksonDataTypeVersion = '2.7.4' junitVersion = '4.12' - testFxVersion = '4.0.+' + testFxVersion = '4.0.7-alpha' monocleVersion = '1.8.0_20' + checkstyleVersion = '8.1' libDir = 'lib' } + checkstyle { + toolVersion = "$checkstyleVersion" + } + jacocoTestReport { reports { xml.enabled false @@ -48,6 +60,7 @@ allprojects { } dependencies { + compile group: 'org.fxmisc.easybind', name: 'easybind', version: '1.0.3' compile "org.controlsfx:controlsfx:$controlsFxVersion" compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonDataTypeVersion" @@ -61,7 +74,7 @@ allprojects { } testCompile "org.testfx:openjfx-monocle:$monocleVersion" } - + sourceSets { main { java { @@ -72,14 +85,10 @@ allprojects { } } } - + shadowJar { archiveName = "addressbook.jar" - manifest { - attributes "Main-Class": "seedu.address.MainApp" - } - destinationDir = file("${buildDir}/jar/") } } @@ -117,6 +126,17 @@ class AddressBookTest extends Test { public AddressBookTest() { forkEvery = 1 systemProperty 'testfx.setup.timeout', '60000' + + /* + * Prints the currently running test's name in the CI's build log, + * so that we can check if tests are being silently skipped or + * stalling the build. + */ + if (System.env.'CI') { + beforeTest { descriptor -> + logger.lifecycle('Running test: ' + descriptor) + } + } } public void setHeadless() { @@ -130,6 +150,8 @@ class AddressBookTest extends Test { task guiTests(type: AddressBookTest) { include 'guitests/**' + include 'systemtests/**' + include 'seedu/address/ui/**' jacoco { destinationFile = new File("${buildDir}/jacoco/test.exec") @@ -139,6 +161,7 @@ task guiTests(type: AddressBookTest) { task nonGuiTests(type: AddressBookTest) { include 'seedu/address/**' + exclude 'seedu/address/ui/**' jacoco { destinationFile = new File("${buildDir}/jacoco/test.exec") @@ -164,4 +187,43 @@ nonGuiTests.mustRunAfter headless guiTests.mustRunAfter headless allTests.mustRunAfter headless -defaultTasks 'clean', 'headless', 'allTests', 'coverage' +asciidoctor { + backends 'html5' + sourceDir 'docs' + outputDir "${buildDir}/docs" + + attributes linkcss: true, + stylesheet: 'gh-pages.css', + 'source-highlighter': 'coderay', + icons: 'font', + experimental: true, + sectlinks: true, + idprefix: '', // for compatibility with GitHub preview + idseparator: '-' +} + +/* + * Copies stylesheets into the directory containing generated HTML files as + * Asciidoctor does not copy linked CSS files to the output directory when rendering. + * This is needed for linked stylesheets and embedded stylesheets which import other files. + */ +task copyStylesheets(type: Copy) { + from "${asciidoctor.sourceDir}/stylesheets" + into "${asciidoctor.outputDir}/html5/stylesheets" +} +asciidoctor.dependsOn copyStylesheets + +task deployOfflineDocs(type: Copy) { + into('src/main/resources/docs') + + from ("${asciidoctor.outputDir}/html5") { + include 'stylesheets/*' + include 'images/*' + include 'UserGuide.html' + } +} + +deployOfflineDocs.dependsOn asciidoctor +processResources.dependsOn deployOfflineDocs + +defaultTasks 'clean', 'headless', 'allTests', 'coverage', 'asciidoctor' diff --git a/config/checkstyle/checkstyle-noframes-sorted.xsl b/config/checkstyle/checkstyle-noframes-sorted.xsl deleted file mode 100644 index 9c0ac3054165..000000000000 --- a/config/checkstyle/checkstyle-noframes-sorted.xsl +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

CheckStyle Audit

Designed for use with CheckStyle and Ant.
-
- - - -
- - - -
- - - - -
- - - - -
- - - - -

Files

- - - - - - - - - - - - - - -
NameErrors
-
- - - - -

File

- - - - - - - - - - - - - - -
Error DescriptionLine
- Back to top -
- - - -

Summary

- - - - - - - - - - - - -
FilesErrors
-
- - - - a - b - - -
- - diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 3bab4e05bbae..253007fe5cc0 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -4,121 +4,121 @@ "http://www.puppycrawl.com/dtds/configuration_1_3.dtd"> - + - + + + + - + - + + + + + + + - + - IMPORT CHECKS + + + + + + + - - - + + + + + + + - - - - - - - - + + - + - + + + + - + + + + + - - + - - + + - - + + - - - + + @@ -128,7 +128,7 @@ - + @@ -139,52 +139,45 @@ - - + + - - + - + - + - + + + + + + - + - - - - @@ -215,12 +208,11 @@ - - + + + + + + @@ -248,12 +245,30 @@ + + + + + + + - MODIFIERS CHECKS + + + + + + + + + RCURLY, SL, SLIST, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/> + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 000000000000..b2e7fdbc3a00 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/config/findbugs/excludeFilter.xml b/config/findbugs/excludeFilter.xml deleted file mode 100644 index 03c15ae4cc81..000000000000 --- a/config/findbugs/excludeFilter.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/config/travis/check-eof-newline.sh b/config/travis/check-eof-newline.sh new file mode 100755 index 000000000000..94cb9ec57b2b --- /dev/null +++ b/config/travis/check-eof-newline.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Checks that all text files end with a newline. + +ret=0 + +for filename in $(git grep --cached -I -l -e '' -- ':/'); do + if [ "$(tail -c 1 "./$filename")" != '' ]; then + line="$(wc -l "./$filename" | cut -d' ' -f1)" + echo "ERROR:$filename:$line: no newline at EOF." + ret=1 + fi +done + +exit $ret diff --git a/config/travis/check-line-endings.sh b/config/travis/check-line-endings.sh new file mode 100755 index 000000000000..3de67ea87f6b --- /dev/null +++ b/config/travis/check-line-endings.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Checks for prohibited line endings. +# Prohibited line endings: \r\n + +git grep --cached -I -n --no-color -P '\r$' -- ':/' | +awk ' + BEGIN { + FS = ":" + OFS = ":" + ret = 0 + } + { + ret = 1 + print "ERROR", $1, $2, " prohibited \\r\\n line ending, use \\n instead." + } + END { + exit ret + } +' diff --git a/config/travis/check-trailing-whitespace.sh b/config/travis/check-trailing-whitespace.sh new file mode 100755 index 000000000000..33841caa81f1 --- /dev/null +++ b/config/travis/check-trailing-whitespace.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Checks for trailing whitespace + +git grep --cached -I -n --no-color -P '[ \t]+$' -- ':/' | +awk ' + BEGIN { + FS = ":" + OFS = ":" + ret = 0 + } + { + # Only warn for markdown files (*.md) to accomodate text editors + # which do not properly handle trailing whitespace. + # (e.g. GitHub web editor) + if ($1 ~ /\.md$/) { + severity = "WARN" + } else { + severity = "ERROR" + ret = 1 + } + print severity, $1, $2, " trailing whitespace." + } + END { + exit ret + } +' diff --git a/config/travis/deploy_github_pages.sh b/config/travis/deploy_github_pages.sh new file mode 100755 index 000000000000..2d8909741ab3 --- /dev/null +++ b/config/travis/deploy_github_pages.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Pushes files generated by Asciidoctor and associated files to gh-pages branch for commits to master branch. + +set -o errexit # exit with nonzero exit code if any line fails + +if [ -z "$GITHUB_TOKEN" ]; then + echo 'GITHUB_TOKEN is not set up in Travis. Skipping deploy.' + exit 0 +fi; + +set -o nounset # exit if variable is unset + +cd build/docs/html5 + +git init +git config user.name 'Deployment Bot (Travis)' +git config user.email 'deploy@travis-ci.org' + +git config credential.helper 'store --file=.git/credentials' +echo "https://${GITHUB_TOKEN}:@github.com" > .git/credentials + +git remote add upstream "https://github.com/${TRAVIS_REPO_SLUG}.git" + +# Reset to gh-pages branch, or create orphan branch if gh-pages does not exist in remote. +if git ls-remote --exit-code --heads upstream gh-pages; then + git fetch --depth=1 upstream gh-pages + git reset upstream/gh-pages +elif [ $? -eq 2 ]; then # exit code of git ls-remote is 2 if branch does not exist + git checkout --orphan gh-pages +else # error occurred + exit $? +fi + +# Exit if there are no changes to gh-pages files. +if changes=$(git status --porcelain) && [ -z "$changes" ]; then + echo 'No changes to GitHub Pages files; exiting.' + exit 0 +fi + +git add -A . +git commit -m "Rebuild pages at ${TRAVIS_COMMIT}" +git push --quiet upstream HEAD:gh-pages diff --git a/config/travis/run-checks.sh b/config/travis/run-checks.sh new file mode 100755 index 000000000000..7aad1e962203 --- /dev/null +++ b/config/travis/run-checks.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Runs all check-* scripts, and returns a non-zero exit code if any of them fail. + +dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) && +ret=0 && +for checkscript in "$dir"/check-*; do + if ! "$checkscript"; then + ret=1 + fi +done +exit $ret diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc new file mode 100644 index 000000000000..27ce8f9b6c66 --- /dev/null +++ b/docs/AboutUs.adoc @@ -0,0 +1,41 @@ += About Us +:relfileprefix: team/ +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:imagesDir: images +:stylesDir: stylesheets + +Project LISA was developed by the https://github.com/CS2103AUG2017-T13-B1[CS2103AUG2017-T13-B1] team. + +{empty} + +We are a 5-man team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. + +== Project Team + +=== WANG CHIH-HAO aka Oscar +image::oscarwang.jpg[width="150", align="left"] +image:githubicon.png[Github Icon, 40, 40, link="https://github.com/OscarWang114"] image:portfolio.png[Portfolio Icon, 40, 40, link="team/oscarwang.adoc"] + +Role: Developer + +Responsibilities: + +=== Pujitha Desiraju +image::PujithaDesiraju.jpg[width="150", align="left"] +{empty}[https://github.com/Pujitha97[github]] [<>] + +Role: Developer + +Responsibilities: + +=== Jaryl Tan aka Juxarius +image::jaryltan.jpg[width="150", align="left"] +image:githubicon.png[Github Icon, 40, 40, link="https://github.com/Juxarius"] image:portfolio.png[Portfolio Icon, 40, 40, link="team/jaryltan.adoc"] + +Role: Developer + +Responsibilities: + +=== RATTANIN SIRIPORNPITAK +image::rsjunior37.jpg[width="150", align="left"] +image:githubicon.png[Github Icon, 40, 40, link="https://github.com/RSJunior37"] image:portfolio.png[Portfolio Icon, 40, 40, link="team/rsjunior.adoc"] + +Role: Developer + +Responsibilities: + +''' diff --git a/docs/AboutUs.md b/docs/AboutUs.md deleted file mode 100644 index 33df65bea583..000000000000 --- a/docs/AboutUs.md +++ /dev/null @@ -1,52 +0,0 @@ -# About Us - -We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). - -## Project Team - -#### [Damith C. Rajapakse](http://www.comp.nus.edu.sg/~damithch)
-
-**Role**: Project Advisor - ------ - -#### [Joshua Lee](http://github.com/lejolly) -
-Role: Developer
-Responsibilities: UI - ------ - -#### [Leow Yijin](http://github.com/yijinl) -
-Role: Developer
-Responsibilities: Data - ------ - -#### [Martin Choo](http://github.com/m133225) -
-Role: Developer
-Responsibilities: Dev Ops - ------ - -#### [Thien Nguyen](https://github.com/ndt93) - Role: Developer
- Responsibilities: Threading - - ----- - -#### [You Liang](http://github.com/yl-coder) -
- Role: Developer
- Responsibilities: UI - - ----- - -# Contributors - -We welcome contributions. See [Contact Us](ContactUs.md) page for more info. - -* [Akshay Narayan](https://github.com/se-edu/addressbook-level4/pulls?q=is%3Apr+author%3Aokkhoy) -* [Sam Yong](https://github.com/se-edu/addressbook-level4/pulls?q=is%3Apr+author%3Amauris) \ No newline at end of file diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc new file mode 100644 index 000000000000..eafdc9574a50 --- /dev/null +++ b/docs/ContactUs.adoc @@ -0,0 +1,6 @@ += Contact Us +:stylesDir: stylesheets + +* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] +* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` diff --git a/docs/ContactUs.md b/docs/ContactUs.md deleted file mode 100644 index 866d0de3fddc..000000000000 --- a/docs/ContactUs.md +++ /dev/null @@ -1,8 +0,0 @@ -# Contact Us - -* **Bug reports, Suggestions** : Post in our [issue tracker](https://github.com/se-edu/addressbook-level4/issues) - if you noticed bugs or have suggestions on how to improve. - -* **Contributing** : We welcome pull requests. Follow the process described [here](https://github.com/oss-generic/process) - -* **Email us** : You can also reach us at `damith [at] comp.nus.edu.sg` \ No newline at end of file diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc new file mode 100644 index 000000000000..4905e4c17701 --- /dev/null +++ b/docs/DeveloperGuide.adoc @@ -0,0 +1,885 @@ += LISA: Levels Integrated Secretarial Agent - Developer Guide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:repoURL: https://github.com/se-edu/addressbook-level4/tree/master + +By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` + +== Setting up + +=== Prerequisites + +. *JDK `1.8.0_60`* or later ++ +[NOTE] +Having any Java 8 version is not enough. + +This app will not work with earlier versions of Java 8. ++ + +. *IntelliJ* IDE ++ +[NOTE] +IntelliJ by default has Gradle and JavaFx plugins installed. + +Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plugins` to re-enable them. + + +=== Setting up the project in your computer + +. Fork this repo, and clone the fork to your computer +. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project dialog first) +. Set up the correct JDK version for Gradle +.. Click `Configure` > `Project Defaults` > `Project Structure` +.. Click `New...` and find the directory of the JDK +. Click `Import Project` +. Locate the `build.gradle` file and select it. Click `OK` +. Click `Open as Project` +. Click `OK` to accept the default settings +. Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. + +This will generate all resources required by the application and tests. + +=== Verifying the setup + +. Run the `seedu.address.MainApp` and try a few commands +. link:#testing[Run the tests] to ensure they all pass. + +=== Configurations to do before writing code + +==== Configuring the coding style + +This project follows https://github.com/oss-generic/process/blob/master/docs/CodingStandards.md[oss-generic coding standards]. IntelliJ's default style is mostly compliant with ours but it uses a different import order from ours. To rectify, + +. Go to `File` > `Settings...` (Windows/Linux), or `IntelliJ IDEA` > `Preferences...` (macOS) +. Select `Editor` > `Code Style` > `Java` +. Click on the `Imports` tab to set the order + +* For `Class count to use import with '\*'` and `Names count to use static import with '*'`: Set to `999` to prevent IntelliJ from contracting the import statements +* For `Import Layout`: The order is `import static all other imports`, `import java.\*`, `import javax.*`, `import org.\*`, `import com.*`, `import all other imports`. Add a `` between each `import` + +Optionally, you can follow the <> document to configure Intellij to check style-compliance as you write code. + +==== Updating documentation to match your fork + +After forking the repo, links in the documentation will still point to the `se-edu/addressbook-level4` repo. If you plan to develop this as a separate product (i.e. instead of contributing to the `se-edu/addressbook-level4`) , you should replace the URL in the variable `repoURL` in `DeveloperGuide.adoc` and `UserGuide.adoc` with the URL of your fork. + +==== Setting up CI + +Set up Travis to perform Continuous Integration (CI) for your fork. See <> to learn how to set it up. + +Optionally, you can set up AppVeyor as a second CI (see <>). + +[NOTE] +Having both Travis and AppVeyor ensures your App works on both Unix-based platforms and Windows-based platforms (Travis is Unix-based and AppVeyor is Windows-based) + +==== Getting started with coding + +When you are ready to start coding, + +1. Get some sense of the overall design by reading the link:#architecture[Architecture] section. +2. Take a look at the section link:#suggested-programming-tasks-to-get-started[Suggested Programming Tasks to Get Started]. + +== Design + +=== Architecture + +image::Architecture.png[width="600"] +_Figure 2.1.1 : Architecture Diagram_ + +The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. + +[TIP] +The `.pptx` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose `Save as picture`. + +`Main` has only one class called link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp`]. It is responsible for, + +* At app launch: Initializes the components in the correct sequence, and connects them up with each other. +* At shut down: Shuts down the components and invokes cleanup method where necessary. + +link:#common-classes[*`Commons`*] represents a collection of classes used by multiple other components. Two of those classes play important roles at the architecture level. + +* `EventsCenter` : This class (written using https://github.com/google/guava/wiki/EventBusExplained[Google's Event Bus library]) is used by components to communicate with other components using events (i.e. a form of _Event Driven_ design) +* `LogsCenter` : Used by many classes to write log messages to the App's log file. + +The rest of the App consists of four components. + +* link:#ui-component[*`UI`*] : The UI of the App. +* link:#logic-component[*`Logic`*] : The command executor. +* link:#model-component[*`Model`*] : Holds the data of the App in-memory. +* link:#storage-component[*`Storage`*] : Reads data from, and writes data to, the hard disk. + +Each of the four components + +* Defines its _API_ in an `interface` with the same name as the Component. +* Exposes its functionality using a `{Component Name}Manager` class. + +For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class. + +image::LogicClassDiagram.png[width="800"] +_Figure 2.1.2 : Class Diagram of the Logic Component_ + +[discrete] +==== Events-Driven nature of the design + +The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete 1`. + +image::SDforDeletePerson.png[width="800"] +_Figure 2.1.3a : Component interactions for `delete 1` command (part 1)_ + +[NOTE] +Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, instead of asking the `Storage` to save the updates to the hard disk. + +The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time. + +image::SDforDeletePersonEventHandling.png[width="800"] +_Figure 2.1.3b : Component interactions for `delete 1` command (part 2)_ + +[NOTE] +Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components. + +The sections below give more details of each component. + +=== UI component + +image::UiClassDiagram.png[width="800"] +_Figure 2.2.1 : Structure of the UI Component_ + +*API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. + +The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] + +The `UI` component, + +* Executes user commands using the `Logic` component. +* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. +* Responds to events raised from various parts of the App and updates the UI accordingly. + +=== Logic component + +image::LogicClassDiagram.png[width="800"] +_Figure 2.3.1 : Structure of the Logic Component_ + +image::LogicCommandClassDiagram.png[width="800"] +_Figure 2.3.2 : Structure of Commands in the Logic Component. This diagram shows finer details concerning `XYZCommand` and `Command` in Figure 2.3.1_ + +*API* : +link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] + +. `Logic` uses the `AddressBookParser` class to parse the user command. +. This results in a `Command` object which is executed by the `LogicManager`. +. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. +. The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. + +Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. + +image::DeletePersonSdForLogic.png[width="800"] +_Figure 2.3.1 : Interactions Inside the Logic Component for the `delete 1` Command_ + +=== Model component + +image::ModelClassDiagram.png[width="800"] +_Figure 2.4.1 : Structure of the Model Component_ + +*API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] + +The `Model`, + +* stores a `UserPref` object that represents the user's preferences. +* stores the Address Book data. +* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* does not depend on any of the other three components. + +=== Storage component + +image::StorageClassDiagram.png[width="800"] +_Figure 2.5.1 : Structure of the Storage Component_ + +*API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] + +The `Storage` component, + +* can save `UserPref` objects in json format and read it back. +* can save the Address Book data in xml format and read it back. + +=== Common classes + +Classes used by multiple components are in the `seedu.addressbook.commons` package. + +== Implementation + +This section describes some noteworthy details on how certain features are implemented. + +// tag::undoredo[] +=== Undo/Redo mechanism + +The undo/redo mechanism is facilitated by an `UndoRedoStack`, which resides inside `LogicManager`. It supports undoing and redoing of commands that modifies the state of the address book (e.g. `add`, `edit`). Such commands will inherit from `UndoableCommand`. + +`UndoRedoStack` only deals with `UndoableCommands`. Commands that cannot be undone will inherit from `Command` instead. The following diagram shows the inheritance diagram for commands: + +image::LogicCommandClassDiagram.png[width="800"] + +As you can see from the diagram, `UndoableCommand` adds an extra layer between the abstract `Command` class and concrete commands that can be undone, such as the `DeleteCommand`. Note that extra tasks need to be done when executing a command in an _undoable_ way, such as saving the state of the address book before execution. `UndoableCommand` contains the high-level algorithm for those extra tasks while the child classes implements the details of how to execute the specific command. Note that this technique of putting the high-level algorithm in the parent class and lower-level steps of the algorithm in child classes is also known as the https://www.tutorialspoint.com/design_pattern/template_pattern.htm[template pattern]. + +Commands that are not undoable are implemented this way: +[source,java] +---- +public class ListCommand extends Command { + @Override + public CommandResult execute() { + // ... list logic ... + } +} +---- + +With the extra layer, the commands that are undoable are implemented this way: +[source,java] +---- +public abstract class UndoableCommand extends Command { + @Override + public CommandResult execute() { + // ... undo logic ... + + executeUndoableCommand(); + } +} + +public class DeleteCommand extends UndoableCommand { + @Override + public CommandResult executeUndoableCommand() { + // ... delete logic ... + } +} +---- + +Suppose that the user has just launched the application. The `UndoRedoStack` will be empty at the beginning. + +The user executes a new `UndoableCommand`, `delete 5`, to delete the 5th person in the address book. The current state of the address book is saved before the `delete 5` command executes. The `delete 5` command will then be pushed onto the `undoStack` (the current state is saved together with the command). + +image::UndoRedoStartingStackDiagram.png[width="800"] + +As the user continues to use the program, more commands are added into the `undoStack`. For example, the user may execute `add n/David ...` to add a new person. + +image::UndoRedoNewCommand1StackDiagram.png[width="800"] + +[NOTE] +If a command fails its execution, it will not be pushed to the `UndoRedoStack` at all. + +The user now decides that adding the person was a mistake, and decides to undo that action using `undo`. + +We will pop the most recent command out of the `undoStack` and push it back to the `redoStack`. We will restore the address book to the state before the `add` command executed. + +image::UndoRedoExecuteUndoStackDiagram.png[width="800"] + +[NOTE] +If the `undoStack` is empty, then there are no other commands left to be undone, and an `Exception` will be thrown when popping the `undoStack`. + +The following sequence diagram shows how the undo operation works: + +image::UndoRedoSequenceDiagram.png[width="800"] + +The redo does the exact opposite (pops from `redoStack`, push to `undoStack`, and restores the address book to the state after the command is executed). + +[NOTE] +If the `redoStack` is empty, then there are no other commands left to be redone, and an `Exception` will be thrown when popping the `redoStack`. + +The user now decides to execute a new command, `clear`. As before, `clear` will be pushed into the `undoStack`. This time the `redoStack` is no longer empty. It will be purged as it no longer make sense to redo the `add n/David` command (this is the behavior that most modern desktop applications follow). + +image::UndoRedoNewCommand2StackDiagram.png[width="800"] + +Commands that are not undoable are not added into the `undoStack`. For example, `list`, which inherits from `Command` rather than `UndoableCommand`, will not be added after execution: + +image::UndoRedoNewCommand3StackDiagram.png[width="800"] + +The following activity diagram summarize what happens inside the `UndoRedoStack` when a user executes a new command: + +image::UndoRedoActivityDiagram.png[width="200"] + +==== Design Considerations + +**Aspect:** Implementation of `UndoableCommand` + +**Alternative 1 (current choice):** Add a new abstract method `executeUndoableCommand()` + +**Pros:** We will not lose any undone/redone functionality as it is now part of the default behaviour. Classes that deal with `Command` do not have to know that `executeUndoableCommand()` exist. + +**Cons:** Hard for new developers to understand the template pattern. + +**Alternative 2:** Just override `execute()` + +**Pros:** Does not involve the template pattern, easier for new developers to understand. + +**Cons:** Classes that inherit from `UndoableCommand` must remember to call `super.execute()`, or lose the ability to undo/redo. + +--- + +**Aspect:** How undo & redo executes + +**Alternative 1 (current choice):** Saves the entire address book. + +**Pros:** Easy to implement. + +**Cons:** May have performance issues in terms of memory usage. + +**Alternative 2:** Individual command knows how to undo/redo by itself. + +**Pros:** Will use less memory (e.g. for `delete`, just save the person being deleted). + +**Cons:** We must ensure that the implementation of each individual command are correct. + +--- + +**Aspect:** Type of commands that can be undone/redone + +**Alternative 1 (current choice):** Only include commands that modifies the address book (`add`, `clear`, `edit`). + +**Pros:** We only revert changes that are hard to change back (the view can easily be re-modified as no data are lost). + +**Cons:** User might think that undo also applies when the list is modified (undoing filtering for example), only to realize that it does not do that, after executing `undo`. + +**Alternative 2:** Include all commands. + +**Pros:** Might be more intuitive for the user. + +**Cons:** User have no way of skipping such commands if he or she just want to reset the state of the address book and not the view. + +**Additional Info:** See our discussion https://github.com/se-edu/addressbook-level4/issues/390#issuecomment-298936672[here]. + +--- + +**Aspect:** Data structure to support the undo/redo commands + +**Alternative 1 (current choice):** Use separate stack for undo and redo + +**Pros:** Easy to understand for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. + +**Cons:** Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `UndoRedoStack`. + +**Alternative 2:** Use `HistoryManager` for undo/redo + +**Pros:** We do not need to maintain a separate stack, and just reuse what is already in the codebase. + +**Cons:** Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. + +// end::undoredo[] + +=== Logging + +We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. + +* The logging level can be controlled using the `logLevel` setting in the configuration file (See link:#configuration[Configuration]) +* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level +* Currently log messages are output through: `Console` and to a `.log` file. + +*Logging Levels* + +* `SEVERE` : Critical problem detected which may possibly cause the termination of the application +* `WARNING` : Can continue, but with caution +* `INFO` : Information showing the noteworthy actions by the App +* `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size + +=== Configuration + +Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file (default: `config.json`). + +== Documentation + +We use asciidoc for writing documentation. + +[NOTE] +We chose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting. + +=== Editing Documentation + +See <> to learn how to render `.adoc` files locally to preview the end result of your edits. +Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your `.adoc` files in real-time. + +=== Publishing Documentation + +See <> to learn how to deploy GitHub Pages using Travis. + +=== Converting Documentation to PDF format + +We use https://www.google.com/chrome/browser/desktop/[Google Chrome] for converting documentation to PDF format, as Chrome's PDF engine preserves hyperlinks used in webpages. + +Here are the steps to convert the project documentation files to PDF format. + +. Follow the instructions in <> to convert the AsciiDoc files in the `docs/` directory to HTML format. +. Go to your generated HTML files in the `build/docs` folder, right click on them and select `Open with` -> `Google Chrome`. +. Within Chrome, click on the `Print` option in Chrome's menu. +. Set the destination to `Save as PDF`, then click `Save` to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below. + +image::chrome_save_as_pdf.png[width="300"] +_Figure 5.6.1 : Saving documentation as PDF files in Chrome_ + +== Testing + +=== Running Tests + +There are three ways to run tests. + +[TIP] +The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies. + +*Method 1: Using IntelliJ JUnit test runner* + +* To run all tests, right-click on the `src/test/java` folder and choose `Run 'All Tests'` +* To run a subset of tests, you can right-click on a test package, test class, or a test and choose `Run 'ABC'` + +*Method 2: Using Gradle* + +* Open a console and run the command `gradlew clean allTests` (Mac/Linux: `./gradlew clean allTests`) + +[NOTE] +See <> for more info on how to run tests using Gradle. + +*Method 3: Using Gradle (headless)* + +Thanks to the https://github.com/TestFX/TestFX[TestFX] library we use, our GUI tests can be run in the _headless_ mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running. + +To run tests in headless mode, open a console and run the command `gradlew clean headless allTests` (Mac/Linux: `./gradlew clean headless allTests`) + +=== Types of tests + +We have two types of tests: + +. *GUI Tests* - These are tests involving the GUI. They include, +.. _System Tests_ that test the entire App by simulating user actions on the GUI. These are in the `systemtests` package. +.. _Unit tests_ that test the individual components. These are in `seedu.address.ui` package. +. *Non-GUI Tests* - These are tests not involving the GUI. They include, +.. _Unit tests_ targeting the lowest level methods/classes. + +e.g. `seedu.address.commons.StringUtilTest` +.. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + +e.g. `seedu.address.storage.StorageManagerTest` +.. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + +e.g. `seedu.address.logic.LogicManagerTest` + + +=== Troubleshooting Testing +**Problem: `HelpWindowTest` fails with a `NullPointerException`.** + +* Reason: One of its dependencies, `UserGuide.html` in `src/main/resources/docs` is missing. +* Solution: Execute Gradle task `processResources`. + +== Dev Ops + +=== Build Automation + +See <> to learn how to use Gradle for build automation. + +=== Continuous Integration + +We use https://travis-ci.org/[Travis CI] and https://www.appveyor.com/[AppVeyor] to perform _Continuous Integration_ on our projects. See <> and <> for more details. + +=== Making a Release + +Here are the steps to create a new release. + +. Update the version number in link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp.java`]. +. Generate a JAR file <>. +. Tag the repo with the version number. e.g. `v0.1` +. https://help.github.com/articles/creating-releases/[Create a new release using GitHub] and upload the JAR file you created. + +=== Managing Dependencies + +A project often depends on third-party libraries. For example, Address Book depends on the http://wiki.fasterxml.com/JacksonHome[Jackson library] for XML parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives. + +a. Include those libraries in the repo (this bloats the repo size) + +b. Require developers to download those libraries manually (this creates extra work for developers) + +[appendix] +== Suggested Programming Tasks to Get Started + +Suggested path for new programmers: + +1. First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in this section link:#improving-each-component[Improving a Component]. + +2. Next, add a feature that touches multiple components to learn how to implement an end-to-end feature across all components. The section link:#creating-a-new-command-code-remark-code[Creating a new command: `remark`] explains how to go about adding such a feature. + +=== Improving each component + +Each individual exercise in this section is component-based (i.e. you would not need to modify the other components to get it to work). + +[discrete] +==== `Logic` component + +[TIP] +Do take a look at the link:#logic-component[Design: Logic Component] section before attempting to modify the `Logic` component. + +. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all persons in the list. ++ +**** +* Hints +** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). +** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. +* Solution +** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. +** See this https://github.com/se-edu/addressbook-level4/pull/590/files[PR] for the full solution. +**** + +[discrete] +==== `Model` component + +[TIP] +Do take a look at the link:#model-component[Design: Model Component] section before attempting to modify the `Model` component. + +. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. ++ +**** +* Hints +** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] API needs to be updated. +** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. +* Solution +** Add the implementation of `deleteTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Loop through each person, and remove the `tag` from each person. +** See this https://github.com/se-edu/addressbook-level4/pull/591/files[PR] for the full solution. +**** + +[discrete] +==== `Ui` component + +[TIP] +Do take a look at the link:#ui-component[Design: UI Component] section before attempting to modify the `UI` component. + +. Use different colors for different tags inside person cards. For example, `friends` tags can be all in grey, and `colleagues` tags can be all in red. ++ +**Before** ++ +image::getting-started-ui-tag-before.png[width="300"] ++ +**After** ++ +image::getting-started-ui-tag-after.png[width="300"] ++ +**** +* Hints +** The tag labels are created inside link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard#initTags(ReadOnlyPerson)`] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. +** Use the .css attribute `-fx-background-color` to add a color. +* Solution +** See this https://github.com/se-edu/addressbook-level4/pull/592/files[PR] for the full solution. +**** + +. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). ++ +**Before** ++ +image::getting-started-ui-result-before.png[width="200"] ++ +**After** ++ +image::getting-started-ui-result-after.png[width="200"] ++ +**** +* Hints +** link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. +** Refer to link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. +* Solution +** Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. +** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(event)`] to react to this event appropriately. +** See this https://github.com/se-edu/addressbook-level4/pull/593/files[PR] for the full solution. +**** + +. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. ++ +**Before** ++ +image::getting-started-ui-status-before.png[width="500"] ++ +**After** ++ +image::getting-started-ui-status-after.png[width="500"] ++ +**** +* Hints +** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! +** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. +* Solution +** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. +** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. +** See this https://github.com/se-edu/addressbook-level4/pull/596/files[PR] for the full solution. +**** + +[discrete] +==== `Storage` component + +[TIP] +Do take a look at the link:#storage-component[Design: Storage Component] section before attempting to modify the `Storage` component. + +. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. ++ +**** +* Hint +** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. +** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] class. +* Solution +** See this https://github.com/se-edu/addressbook-level4/pull/594/files[PR] for the full solution. +**** + +=== Creating a new command: `remark` + +By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app. + +==== Description +Edits the remark for a person specified in the `INDEX`. + +Format: `remark INDEX r/[REMARK]` + +Examples: + +* `remark 1 r/Likes to drink coffee.` + +Edits the remark for the first person to `Likes to drink coffee.` +* `remark 1 r/` + +Removes the remark for the first person. + +==== Step-by-step Instructions + +===== [Step 1] Logic: Teach the app to accept 'remark' which does nothing +Let's start by teaching the application how to parse a `remark` command. We will add the logic of `remark` later. + +**Main:** + +. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/UndoableCommand.java[`UndoableCommand`]. Upon execution, it should just throw an `Exception`. +. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. + +**Tests:** + +. Add `RemarkCommandTest` that tests that `executeUndoableCommand()` throws an Exception. +. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. + +===== [Step 2] Logic: Teach the app to accept 'remark' arguments +Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` + +**Main:** + +. Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. +. Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. +. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. + +**Tests:** + +. Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. +. Add `RemarkCommandParserTest` that tests different boundary values +for `RemarkCommandParser`. +. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. + +===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` +Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. + +**Main:** + +. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/PersonListCard.fxml[`PersonListCard.fxml`]. +. Add FXML annotation in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] to tie the variable to the actual label. + +**Tests:** + +. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. + +===== [Step 4] Model: Add `Remark` class +We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`ReadOnlyPerson`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. + +**Main:** + +. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). +. Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. + +**Tests:** + +. Add test for `Remark`, to test the `Remark#equals()` method. + +===== [Step 5] Model: Modify `ReadOnlyPerson` to support a `Remark` field +Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`ReadOnlyPerson`]. + +**Main:** + +. Add three methods `setRemark(Remark)`, `getRemark()` and `remarkProperty()`. Be sure to implement these newly created methods in link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`Person`], which implements the link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`ReadOnlyPerson`] interface. +. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). +. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `addressBook.xml` so that the application will load the sample data when you launch it.) + +===== [Step 6] Storage: Add `Remark` field to `XmlAdaptedPerson` class +We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/XmlAdaptedPerson.java[`XmlAdaptedPerson`] to include a `Remark` field so that it will be saved. + +**Main:** + +. Add a new Xml field for `Remark`. +. Be sure to modify the logic of the constructor and `toModelType()`, which handles the conversion to/from link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`ReadOnlyPerson`]. + +**Tests:** + +. Fix `validAddressBook.xml` such that the XML tests will not fail due to a missing `` element. + +===== [Step 7] Ui: Connect `Remark` field to `PersonCard` +Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. + +**Main:** + +. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard#bindListeners()`] to add the binding for `remark`. + +**Tests:** + +. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the remark label. +. In link:{repoURL}/src/test/java/seedu/address/ui/PersonCardTest.java[`PersonCardTest`], call `personWithTags.setRemark(ALICE.getRemark())` to test that changes in the link:{repoURL}/src/main/java/seedu/address/model/person/ReadOnlyPerson.java[`Person`] 's remark correctly updates the corresponding link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]. + +===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic +We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. + +**Main:** + +. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. + +**Tests:** + +. Update `RemarkCommandTest` to test that the `execute()` logic works. + +==== Full Solution + +See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. + +[appendix] +== User Stories + +Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` + +[width="59%",cols="22%,<23%,<25%,<30%",options="header",] +|======================================================================= +|Priority |As a ... |I want to ... |So that I can... +|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App + +|`* * *` |user |add a new person by providing only his/her name| + +|`* * *` |user |delete a person |remove entries that I no longer need + +|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list + +|`* * *` |careless user| undo/redo previous command | to easily correct the mistake I made + +|`* * *` |user | add/delete tags without overwriting/rewriting the entire tag list | + +|`* *` |user | search with partial keyword | find details I'm not sure of + +|`* *` |user | import contact from other types of file| + +|`* *` |user | add birthday to contacts | + +|`* *` |user |hide link:#private-contact-detail[private contact details] by default |minimize chance of someone else seeing them by accident + +|`* *` |artistic user| customize the GUI without extensive programming knowledge| + +|`* *` |forgetful user| have reminder for upcoming link:#event[events]| + +|`* *` |lazy user | auto-complete my command| reduce typing + +|`* *` |secured user| set password to unlock the addressbook| have enhanced security + +|`* *` |unorganized user| add link:#appointment[appointment] to contacts| + +|`* *` |user with international friends| add country/area code to phone number| know exactly where the number connects to + +|`*` |user with many persons in the address book |sort persons by name |locate a person easily + +|`*` |user | check last modified date for a contact| identify possible outdated contacts + +|`*` |lazy user| create an email by clicking on email address on person card| send email on the fly + + +|======================================================================= + +{More to be added} + +[appendix] +== Use Cases + +(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) + +[discrete] +=== Use case: Delete person + +*MSS* + +1. User requests to list persons +2. AddressBook shows a list of persons +3. User requests to delete a specific person in the list +4. AddressBook deletes the person ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. + +* 3a. The given index is invalid. ++ +[none] +** 3a1. AddressBook shows an error message. ++ +Use case resumes at step 2. + +=== Use case: Filter contact with search box + +*MSS* + +1. User types the desired contact partially into the search box +2. AddressBooks shows specific contacts that matched the partial search ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User input too few partial keywords ++ +[none] +** 1a1. AddressBook shows filtered list with too many contacts +** 1a2. User extends the keyword to narrow down the search further ++ +Use case resumes at step 2. ++ +[none] +* 1b. No match for given keywords ++ +[none] +** 1b1. AddressBook shows empty contact panel ++ +Use case ends. + + +{More to be added} + +[appendix] +== Non Functional Requirements + +. Should work on any link:#mainstream-os[mainstream OS] as long as it has Java `1.8.0_60` or higher installed. +. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. + +{More to be added} + +[appendix] +== Glossary + +[[appointment]] +Appointment + +.... +Agenda, Venue, Date and Time +.... + +[[event]] +Event + +.... +Things specific to a contact which requires attention. eg: appointment, birthday, etc. +.... + +[[mainstream-os]] +Mainstream OS + +.... +Windows, Linux, Unix, OS-X +.... + +[[private-contact-detail]] +Private contact detail + +.... +A contact detail that is not meant to be shared with others +.... + + +[appendix] +== Product Survey + +*Product Name* + +Author: ... + +Pros: + +* ... +* ... + +Cons: + +* ... +* ... diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md deleted file mode 100644 index bc710ed45eb9..000000000000 --- a/docs/DeveloperGuide.md +++ /dev/null @@ -1,316 +0,0 @@ -# Developer Guide - -* [Setting Up](#setting-up) -* [Design](#design) -* [Implementation](#implementation) -* [Testing](#testing) -* [Dev Ops](#dev-ops) -* [Appendix A: User Stories](#appendix-a--user-stories) -* [Appendix B: Use Cases](#appendix-b--use-cases) -* [Appendix C: Non Functional Requirements](#appendix-c--non-functional-requirements) -* [Appendix D: Glossary](#appendix-d--glossary) -* [Appendix E : Product Survey](#appendix-e-product-survey) - - -## Setting up - -#### Prerequisites - -1. **JDK `1.8.0_60`** or later
- - > Having any Java 8 version is not enough.
- This app will not work with earlier versions of Java 8. - -2. **Eclipse** IDE -3. **e(fx)clipse** plugin for Eclipse (Do the steps 2 onwards given in - [this page](http://www.eclipse.org/efxclipse/install.html#for-the-ambitious)) -4. **Buildship Gradle Integration** plugin from the Eclipse Marketplace - - -#### Importing the project into Eclipse - -0. Fork this repo, and clone the fork to your computer -1. Open Eclipse (Note: Ensure you have installed the **e(fx)clipse** and **buildship** plugins as given - in the prerequisites above) -2. Click `File` > `Import` -3. Click `Gradle` > `Gradle Project` > `Next` > `Next` -4. Click `Browse`, then locate the project's directory -5. Click `Finish` - - > * If you are asked whether to 'keep' or 'overwrite' config files, choose to 'keep'. - > * Depending on your connection speed and server load, it can even take up to 30 minutes for the set up to finish - (This is because Gradle downloads library files from servers during the project set up process) - > * If Eclipse auto-changed any settings files during the import process, you can discard those changes. - -## Design - -### Architecture - -
-The **_Architecture Diagram_** given above explains the high-level design of the App. -Given below is a quick overview of each component. - -`Main` has only one class called [`MainApp`](../src/main/java/seedu/address/MainApp.java). It is responsible for, -* At app launch: Initializes the components in the correct sequence, and connect them up with each other. -* At shut down: Shuts down the components and invoke cleanup method where necessary. - -[**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. -Two of those classes play important roles at the architecture level. -* `EventsCentre` : This class (written using [Google's Event Bus library](https://github.com/google/guava/wiki/EventBusExplained)) - is used by components to communicate with other components using events (i.e. a form of _Event Driven_ design) -* `LogsCenter` : Used by many classes to write log messages to the App's log file. - -The rest of the App consists four components. -* [**`UI`**](#ui-component) : The UI of tha App. -* [**`Logic`**](#logic-component) : The command executor. -* [**`Model`**](#model-component) : Holds the data of the App in-memory. -* [**`Storage`**](#storage-component) : Reads data from, and writes data to, the hard disk. - -Each of the four components -* Defines its _API_ in an `interface` with the same name as the Component. -* Exposes its functionality using a `{Component Name}Manager` class. - -For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` -interface and exposes its functionality using the `LogicManager.java` class.
-
- -The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the -command `delete 3`. - - - ->Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, - instead of asking the `Storage` to save the updates to the hard disk. - -The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates -being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time.
- - -> Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having - to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct - coupling between components. - -The sections below give more details of each component. - -### UI component - -
- -**API** : [`Ui.java`](../src/main/java/seedu/address/ui/Ui.java) - -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, -`StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class -and they can be loaded using the `UiPartLoader`. - -The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files - that are in the `src/main/resources/view` folder.
- For example, the layout of the [`MainWindow`](../src/main/java/seedu/address/ui/MainWindow.java) is specified in - [`MainWindow.fxml`](../src/main/resources/view/MainWindow.fxml) - -The `UI` component, -* Executes user commands using the `Logic` component. -* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. -* Responds to events raised from various parts of the App and updates the UI accordingly. - -### Logic component - -
- -**API** : [`Logic.java`](../src/main/java/seedu/address/logic/Logic.java) - -1. `Logic` uses the `Parser` class to parse the user command. -2. This results in a `Command` object which is executed by the `LogicManager`. -3. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. -4. The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. - -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` - API call.
-
- -### Model component - -
- -**API** : [`Model.java`](../src/main/java/seedu/address/model/Model.java) - -The `Model`, -* stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes a `UnmodifiableObservableList` that can be 'observed' e.g. the UI can be bound to this list - so that the UI automatically updates when the data in the list change. -* does not depend on any of the other three components. - -### Storage component - -
- -**API** : [`Storage.java`](../src/main/java/seedu/address/storage/Storage.java) - -The `Storage` component, -* can save `UserPref` objects in json format and read it back. -* can save the Address Book data in xml format and read it back. - -### Common classes - -Classes used by multiple components are in the `seedu.addressbook.commons` package. - -## Implementation - -### Logging - -We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels -and logging destinations. - -* The logging level can be controlled using the `logLevel` setting in the configuration file - (See [Configuration](#configuration)) -* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to - the specified logging level -* Currently log messages are output through: `Console` and to a `.log` file. - -**Logging Levels** - -* `SEVERE` : Critical problem detected which may possibly cause the termination of the application -* `WARNING` : Can continue, but with caution -* `INFO` : Information showing the noteworthy actions by the App -* `FINE` : Details that is not usually noteworthy but may be useful in debugging - e.g. print the actual list instead of just its size - -### Configuration - -Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file -(default: `config.json`): - - -## Testing - -Tests can be found in the `./src/test/java` folder. - -**In Eclipse**: -> If you are not using a recent Eclipse version (i.e. _Neon_ or later), enable assertions in JUnit tests - as described [here](http://stackoverflow.com/questions/2522897/eclipse-junit-ea-vm-option). - -* To run all tests, right-click on the `src/test/java` folder and choose - `Run as` > `JUnit Test` -* To run a subset of tests, you can right-click on a test package, test class, or a test and choose - to run as a JUnit test. - -**Using Gradle**: -* See [UsingGradle.md](UsingGradle.md) for how to run tests using Gradle. - -We have two types of tests: - -1. **GUI Tests** - These are _System Tests_ that test the entire App by simulating user actions on the GUI. - These are in the `guitests` package. - -2. **Non-GUI Tests** - These are tests not involving the GUI. They include, - 1. _Unit tests_ targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.UrlUtilTest` - 2. _Integration tests_ that are checking the integration of multiple code units - (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` - 3. Hybrids of unit and integration tests. These test are checking multiple code units as well as - how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` - -**Headless GUI Testing** : -Thanks to the [TestFX](https://github.com/TestFX/TestFX) library we use, - our GUI tests can be run in the _headless_ mode. - In the headless mode, GUI tests do not show up on the screen. - That means the developer can do other things on the Computer while the tests are running.
- See [UsingGradle.md](UsingGradle.md#running-tests) to learn how to run tests in headless mode. - -## Dev Ops - -### Build Automation - -See [UsingGradle.md](UsingGradle.md) to learn how to use Gradle for build automation. - -### Continuous Integration - -We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our projects. -See [UsingTravis.md](UsingTravis.md) for more details. - -### Making a Release - -Here are the steps to create a new release. - - 1. Generate a JAR file [using Gradle](UsingGradle.md#creating-the-jar-file). - 2. Tag the repo with the version number. e.g. `v0.1` - 2. [Crete a new release using GitHub](https://help.github.com/articles/creating-releases/) - and upload the JAR file your created. - -### Managing Dependencies - -A project often depends on third-party libraries. For example, Address Book depends on the -[Jackson library](http://wiki.fasterxml.com/JacksonHome) for XML parsing. Managing these _dependencies_ -can be automated using Gradle. For example, Gradle can download the dependencies automatically, which -is better than these alternatives.
-a. Include those libraries in the repo (this bloats the repo size)
-b. Require developers to download those libraries manually (this creates extra work for developers)
- -## Appendix A : User Stories - -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` - - -Priority | As a ... | I want to ... | So that I can... --------- | :-------- | :--------- | :----------- -`* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App -`* * *` | user | add a new person | -`* * *` | user | delete a person | remove entries that I no longer need -`* * *` | user | find a person by name | locate details of persons without having to go through the entire list -`* *` | user | hide [private contact details](#private-contact-detail) by default | minimize chance of someone else seeing them by accident -`*` | user with many persons in the address book | sort persons by name | locate a person easily - -{More to be added} - -## Appendix B : Use Cases - -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) - -#### Use case: Delete person - -**MSS** - -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person
-Use case ends. - -**Extensions** - -2a. The list is empty - -> Use case ends - -3a. The given index is invalid - -> 3a1. AddressBook shows an error message
- Use case resumes at step 2 - -{More to be added} - -## Appendix C : Non Functional Requirements - -1. Should work on any [mainstream OS](#mainstream-os) as long as it has Java `1.8.0_60` or higher installed. -2. Should be able to hold up to 1000 persons. -3. Should come with automated unit tests and open source code. -4. Should favor DOS style commands over Unix-style commands. - -{More to be added} - -## Appendix D : Glossary - -##### Mainstream OS - -> Windows, Linux, Unix, OS-X - -##### Private contact detail - -> A contact detail that is not meant to be shared with others - -## Appendix E : Product Survey - -{TODO: Add a summary of competing products} - diff --git a/docs/LearningOutcomes.adoc b/docs/LearningOutcomes.adoc new file mode 100644 index 000000000000..df1bdb07fc01 --- /dev/null +++ b/docs/LearningOutcomes.adoc @@ -0,0 +1,266 @@ += Learning Outcomes +:toc: macro +:toc-title: +:toclevels: 1 +:sectnums: +:sectnumlevels: 1 +:imagesDir: images +:stylesDir: stylesheets +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:repoURL: https://github.com/se-edu/addressbook-level4/tree/master + +After studying this code and completing the corresponding exercises, you should be able to, + +toc::[] + +''' + +== Use High-Level Designs `[LO-HighLevelDesign]` + +Note how the <> describes the high-level design using an _Architecture Diagrams_ and high-level sequence diagrams. + +*Resources* + +* https://se-edu.github.io/se-book/architecture/[se-edu/se-book: Design: Architecture] +* https://se-edu.github.io/se-book/design/introduction/multilevelDesign/[se-edu/se-book: Design: Introduction: Multi-Level Design] + +''' + +== Use Event-Driven Programming `[LO-EventDriven]` + +Note how the <> uses events to communicate with components without needing a direct coupling. Also note how the link:{repoURL}/src/main/java/seedu/address/commons/core/index/EventsCenter.java[`EventsCenter.java`] acts as an event dispatcher to facilitate communication between event creators and event consumers. + +*Resources* + +* https://se-edu.github.io/se-book/architecture/architecturalStyles/eventDriven/[se-edu/se-book: Design: Architecture: Architecture Styles: Event-Driven Architectural Style] + +''' + +== Use API Design `[LO-ApiDesign]` + +Note how components of AddressBook have well-defined APIs. For example, the API of the `Logic` component is given in the link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] +image:LogicClassDiagram.png[width="800"] + +*Resources* + +* https://se-edu.github.io/se-book/reuse/apis/[se-edu/se-book: Implementation: Reuse: APIs] + +''' + +== Use Assertions `[LO-Assertions]` + +Note how the AddressBook app uses Java ``assert``s to verify assumptions. + +*Resources* + +* https://se-edu.github.io/se-book/errorHandling/assertions/[se-edu/se-book: Implementation: Error Handling: Assertions] + +=== Exercise: Add more assertions + +* Make sure assertions are enabled in your IDE by forcing an assertion failure (e.g. add `assert false;` somewhere in the code and run the code to ensure the runtime reports an assertion failure). +* Add more assertions to AddressBook as you see fit. + + +''' + +== Use Logging `[LO-Logging]` + +Note <>. + +*Resources* + +* https://se-edu.github.io/se-book/errorHandling/logging/[se-edu/se-book: Implementation: Error Handling: Logging] + +=== Exercise: Add more logging + +Add more logging to AddressBook as you see fit. + + +''' + +== Use Defensive Coding `[LO-DefensiveCoding]` + +Note how AddressBook uses the `ReadOnly*` interfaces to prevent objects being modified by clients who are not supposed to modify them. + +*Resources* + +* https://se-edu.github.io/se-book/errorHandling/defensiveProgramming/[se-edu/se-book: Implementation: Error Handling: Defensive Programming] + +=== Exercise: identify more places for defensive coding + +Analyze the AddressBook code/design to identify, + +* where defensive coding is used +* where the code can be more defensive + +''' + +== Use Build Automation `[LO-BuildAutomation]` + +Note <>. + +*Resources* + +* https://se-edu.github.io/se-book/integration/buildAutomation/what/[se-edu/se-book: Implementation: Integration: Build Automation: What] + +=== Exercise: Use gradle to run tasks + +* Use gradle to do these tasks: Run all tests in headless mode, build the jar file. + +=== Exercise: Use gradle to manage dependencies + +* Note how the build script `build.gradle` file manages third party dependencies such as ControlsFx. Update that file to manage a third-party library dependency. + + +''' + +== Use Continuous Integration `[LO-ContinuousIntegration]` + +Note <>. (https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]]) + +*Resources* + +* https://se-edu.github.io/se-book/integration/buildAutomation/continuousIntegrationDeployment/[se-edu/se-book: Implementation: Integration: Build Automation: CI & CD] + +=== Exercise: Use Travis in your own project + +* Set up Travis to perform CI on your own fork. + + +''' + +== Use Code Coverage `[LO-CodeCoverage]` + +Note how our CI server <>. (https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]]) After setting up Coveralls for your project, you can visit Coveralls website to find details about the coverage of code pushed to your repo. https://coveralls.io/github/se-edu/addressbook-level4?branch=master[Here] is an example. + +*Resources* + +* https://se-edu.github.io/se-book/testing/testCoverage/[se-edu/se-book: QA: Testing: Test Coverage] + +=== Exercise: Use the IDE to measure coverage locally + +* Use the IDE to measure code coverage of your tests. + +''' + +== Apply Test Case Design Heuristics `[LO-TestCaseDesignHeuristics]` + +The link:{repoURL}/src/test/java/seedu/address/commons/util/StringUtilTest.java[`StringUtilTest.java`] +class gives some examples of how to use _Equivalence Partitions_, _Boundary Value Analysis_, and _Test Input Combination Heuristics_ to improve the efficiency and effectiveness of test cases testing the link:../src/main/java/seedu/address/commons/util/StringUtil.java[`StringUtil.java`] class. + +*Resources* + +* https://se-edu.github.io/se-book/testCaseDesign/[se-edu/se-book: QA: Test Case Design] + +=== Exercise: Apply Test Case Design Heuristics to other places + +* Use the test case design heuristics mentioned above to improve test cases in other places. + +''' + +== Write Integration Tests `[LO-IntegrationTests]` + +Consider the link:{repoURL}/src/test/java/seedu/address/storage/StorageManagerTest.java[`StorageManagerTest.java`] class. + +* Test methods `prefsReadSave()` and `addressBookReadSave()` are integration tests. Note how they simply test if The `StorageManager` class is correctly wired to its dependencies. +* Test method `handleAddressBookChangedEvent_exceptionThrown_eventRaised()` is a unit test because it uses _dependency injection_ to isolate the SUT `StorageManager#handleAddressBookChangedEvent(...)` from its dependencies. + +Compare the above with link:{repoURL}/src/test/java/seedu/address/logic/LogicManagerTest.java[`LogicManagerTest`]. Some of the tests in that class (e.g. `execute_*` methods) are neither integration nor unit tests. They are _integration + unit_ tests because they not only check if the LogicManager is correctly wired to its dependencies, but also checks the working of its dependencies. For example, the following two lines test the `LogicManager` but also the `Parser`. + +[source,java] +---- +@Test +public void execute_invalidCommandFormat_throwsParseException() { + ... + assertParseException(invalidCommand, MESSAGE_UNKNOWN_COMMAND); + assertHistoryCorrect(invalidCommand); +} +---- + +*Resources* + +* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] + +=== Exercise: Write unit and integration tests for the same method. + +* Write a unit test for a high-level method somewhere in the code base (or a new method you wrote). +* Write an integration test for the same method. + +''' + +== Write System Tests `[LO-SystemTesting]` + +Note how tests below `src/test/java/systemtests` package (e.g link:{repoURL}/src/test/java/systemtests/AddCommandSystemTest.java[`AddCommandSystemTest.java`]) are system tests because they test the entire system end-to-end. + +*Resources* + +* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] + +=== Exercise: Write more system tests + +* Write system tests for the new features you add. + +''' + +== Automate GUI Testing `[LO-AutomateGuiTesting]` + +Note how this project uses TextFX library to automate GUI testing, including <>. + +=== Exercise: Write more automated GUI tests + +* Covered by `[LO-SystemTesting]` + +''' + +== Apply Design Patterns `[LO-DesignPatterns]` + +Here are some example design patterns used in the code base. + +* *Singleton Pattern* : link:{repoURL}/src/main/java/seedu/address/commons/core/EventsCenter.java[`EventsCenter.java`] is Singleton class. Its single instance can be accessed using the `EventsCenter.getInstance()` method. +* *Facade Pattern* : link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager.java`] is not only shielding the internals of the Storage component from outsiders, it is mostly redirecting method calls to its internal components (i.e. minimal logic in the class itself). Therefore, `StorageManager` can be considered a Facade class. +* *Command Pattern* : The link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command.java`] and its sub classes implement the Command Pattern. +* *Observer Pattern* : The <> used by this code base employs the Observer pattern. For example, objects that are interested in events need to have the `@Subscribe` annotation in the class (this is similar to implementing an `\<>` interface) and register with the `EventsCenter`. When something noteworthy happens, an event is raised and the `EventsCenter` notifies all relevant subscribers. Unlike in the Observer pattern in which the `\<>` class is notifying all `\<>` objects, here the `\<>` classes simply raises an event and the `EventsCenter` takes care of the notifications. +* *MVC Pattern* : +** The 'View' part of the application is mostly in the `.fxml` files in the `src/main/resources/view` folder. +** `Model` component contains the 'Model'. However, note that it is possible to view the `Logic` as the model because it hides the `Model` behind it and the view has to go through the `Logic` to access the `Model`. +** Sub classes of link:{repoURL}/src/main/java/seedu/address/ui/UiPart.java[`UiPart`] (e.g. `PersonListPanel` ) act as 'Controllers', each controlling some part of the UI and communicating with the 'Model' (via the `Logic` component which sits between the 'Controller' and the 'Model'). +* *Abstraction Occurrence Pattern* : Not currently used in the app. + +*Resources* + +* https://se-edu.github.io/se-book/designPatterns/[se-edu/se-book: Design: Design Patterns] + +=== Exercise: Discover other possible applications of the patterns + +* Find other possible applications of the patterns to improve the current design. e.g. where else in the design can you apply the Singleton pattern? +* Discuss pros and cons of applying the pattern in each of the situations you found in the previous step. + +=== Exercise: Find more applicable patterns + +* Learn other _Gang of Four_ Design patterns to see if they are applicable to the app. + +''' + +== Use Static Analysis `[LO-StaticAnalysis]` + +Note how this project uses the http://checkstyle.sourceforge.net/[CheckStyle] static analysis tool to confirm compliance with the coding standard. + +*Resources* + +* https://se-edu.github.io/se-book/qualityAssurance/staticAnalysis/[se-edu/se-book: QA: Static Analysis] + +=== Exercise: Use CheckStyle locally to check style compliance + +* Install the CheckStyle plugin for your IDE and use it to check compliance of your code with our style rules (given in `/config/checkstyle/checkstyle.xml`). + +''' + +== Do Code Reviews `[LO-CodeReview]` + +* Note how some PRs in this project have been reviewed by other developers. Here is an https://github.com/se-edu/addressbook-level4/pull/147[example]. +* Also note how we have used https://www.codacy.com[Codacy] to do automate some part of the code review workload (https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]]) + + +=== Exercise: Review a PR + +* Review PRs created by team members. diff --git a/docs/LearningOutcomes.md b/docs/LearningOutcomes.md deleted file mode 100644 index 31f26a37b480..000000000000 --- a/docs/LearningOutcomes.md +++ /dev/null @@ -1,20 +0,0 @@ -# Learning Outcomes -After studying this code and completing the corresponding exercises, you should be able to, - -1. [Use High-Level Designs `[LO-HighLevelDesign]`](#use-high-level-designs-lo-highleveldesign) - - ------------------------------------------------------------------------------------------------------- - -## Use High-Level Designs `[LO-HighLevelDesign]` -Note how the [Developer Guide](DeveloperGuide.md#design) describes the high-level design using an -_Architecture Diagrams_ and high-level sequence diagrams. - -#### Exercise: Add more user stories - -* ... - ------------------------------------------------------------------------------------------------------- - -{More to be added} - diff --git a/docs/TestDocument.adoc b/docs/TestDocument.adoc new file mode 100644 index 000000000000..bb2ea274ca8b --- /dev/null +++ b/docs/TestDocument.adoc @@ -0,0 +1,61 @@ += Test Document +:toc: +:toc-placement: preamble +:imagesDir: images +:stylesDir: stylesheets + +This is a test asciidoc document to check the setup for asciidoc documentation. + +== Heading + +=== Subheading + +.Bulleted lists +* *bold* +** _italics_ +*** `code` +** [red]#red# + +.Numbered lists +. first +.. first i +.. first ii +.. first iii +. second +. third +.. third i +... third i i + +[NOTE] +==== +This is a note. +==== + +[WARNING] +==== +This is a warning. +==== + +[role="details"] +**** +This is a custom block. +**** + + +**** +This is a sidebar block. +**** + +=== Subheading II + +This tests line break. + +This should be on a new line. + +http://google.com[This] is a link to an external site. + +<> is a link to the previous section. + +image::Architecture.png[title="Architecture diagram", width="400"] +____ +This is a quote block. +____ diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc new file mode 100644 index 000000000000..7bbf9d7af81d --- /dev/null +++ b/docs/UserGuide.adoc @@ -0,0 +1,276 @@ += LISA: Levels Integrated Secretarial Agent - User Guide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:repoURL: https://github.com/se-edu/addressbook-level4 + +By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` + +== Quick Start + +. Ensure you have Java version `1.8.0_60` or later installed in your Computer. ++ +[NOTE] +Having any Java 8 version is not enough. + +This app will not work with earlier versions of Java 8. ++ +. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for your Address Book. +. Double-click the file to start the app. The GUI should appear in a few seconds. ++ +image::Ui.png[width="790"] ++ +. Type the command in the command box and press kbd:[Enter] to execute it. + +e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. +. Some example commands you can try: + +* *`list`* : lists all contacts +* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. +* **`delete`**`3` : deletes the 3rd contact shown in the current list +* *`exit`* : exits the app + +. Refer to the link:#features[Features] section below for details of each command. + +== Features + +==== +*Command Format* + +* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +==== + +=== Viewing help : `help` + +Format: `help` + +=== Adding a person: `add` + +Adds a person to the address book + +Format: `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +[TIP] +A person must have a name and can have any number of tags (including 0) + +Examples: + +* `add n/John Doe` +* `add n/Mary Jane p/98765432 e/maryjane@example.com a/Mary street, block 123, #01-01` +* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison t/criminal` + +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +=== Editing a person : `edit` + +Edits an existing person in the address book. + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG] [dt/TAG_TO_DELETE]...` + +**** +* Edits the person at the specified `INDEX`. The index refers to the index number shown in the last person listing. The index *must be a positive integer* 1, 2, 3, ... +* At least one of the optional fields must be provided. +* Existing values will be updated to the input values. +* When editing tags, tags will be added on to the current existing tags. +* You can remove all the person's tags by typing dt/all +**** + +Examples: + +* `edit 1 p/91234567 e/johndoe@example.com` + +Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. +* `edit 2 n/Betsy Crower t/` + +Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. + +=== Locating persons by name: `find` + +Finds persons whose names contain any of the given keywords. + +Format: `find KEYWORD [MORE_KEYWORDS]` + +**** +* The search is case insensitive. e.g `hans` will match `Hans` +* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` +* Only the name is searched. +* Only full words will be matched e.g. `Han` will not match `Hans` +* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +**** + +Examples: + +* `find John` + +Returns `john` and `John Doe` +* `find Betsy Tim John` + +Returns any person having names `Betsy`, `Tim`, or `John` + +=== Locating persons by the first few alphabets of the name: `pfind` + +Finds persons whose names begins with the given keywords. + +Format: `pfind PARTIALNAME [MORE_PARTIALNAMES]` + +**** +* The search is case insensitive. e.g `ha` will match `Hans` +* Only search for given name. +* Both partial or full name will be matched. e.g. A or Alice will both return `Alice` +* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Ha B` will return `Hans Gruber`, `Bo Yang` +**** + +Examples: + +* `pfind Jo` + +Returns any name starting with Jo, eg: `John`, `Joe` or `Joy` + +Alternatively, this command can perform by directly inputting `PARTIALNAME` into the search box right above the contact panel. + +image::searchbox.png[width="380"] + +=== Deleting a person : `delete` + +Deletes the specified person from the address book. + +Format: `delete INDEX` + +**** +* Deletes the person at the specified `INDEX`. +* The index refers to the index number shown in the most recent listing. +* The index *must be a positive integer* 1, 2, 3, ... +**** + +Examples: + +* `list` + +`delete 2` + +Deletes the 2nd person in the address book. +* `find Betsy` + +`delete 1` + +Deletes the 1st person in the results of the `find` command. + +=== Selecting a person : `select` + +Selects the person identified by the index number used in the last person listing. + +Format: `select INDEX` + +**** +* Selects the person and loads the Google search page the person at the specified `INDEX`. +* The index refers to the index number shown in the most recent listing. +* The index *must be a positive integer* `1, 2, 3, ...` +**** + +Examples: + +* `list` + +`select 2` + +Selects the 2nd person in the address book. +* `find Betsy` + +`select 1` + +Selects the 1st person in the results of the `find` command. + +=== Listing entered commands : `history` + +Lists all the commands that you have entered in reverse chronological order. + +Format: `history` + +[NOTE] +==== +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +==== + +// tag::undoredo[] +=== Undoing previous command : `undo` + +Restores the address book to the state before the previous _undoable_ command was executed. + +Format: `undo` + +[NOTE] +==== +Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +==== + +Examples: + +* `delete 1` + +`list` + +`undo` (reverses the `delete 1` command) + + +* `select 1` + +`list` + +`undo` + +The `undo` command fails as there are no undoable commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + + +=== Redoing the previously undone command : `redo` + +Reverses the most recent `undo` command. + +Format: `redo` + +Examples: + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + + +* `delete 1` + +`redo` + +The `redo` command fails as there are no `undo` commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + +`redo` (reapplies the `clear` command) + +// end::undoredo[] + +=== Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== Saving the data + +Address book data are saved in the hard disk automatically after any command that changes the data. + +There is no need to save manually. + +== FAQ + +*Q*: How do I transfer my data to another Computer? + +*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Address Book folder. + +== Command Summary + +* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + +e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +* *Clear* : `clear` +* *Delete* : `delete INDEX` + +e.g. `delete 3` +* *Edit* : `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +e.g. `edit 2 n/James Lee e/jameslee@example.com` +* *Find* : `find KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` +* *Partial Find* : `pfind PARTIALNAME [MORE_PARTIALNAMES]` + +e.g. `pfind Ja Carl Al` +* *List* : `list` +* *Help* : `help` +* *Select* : `select INDEX` + +e.g.`select 2` +* *History* : `history` +* *Undo* : `undo` +* *Redo* : `redo` diff --git a/docs/UserGuide.md b/docs/UserGuide.md deleted file mode 100644 index 0cf4b84f7470..000000000000 --- a/docs/UserGuide.md +++ /dev/null @@ -1,134 +0,0 @@ -# User Guide - -* [Quick Start](#quick-start) -* [Features](#features) -* [FAQ](#faq) -* [Command Summary](#command-summary) - -## Quick Start - -0. Ensure you have Java version `1.8.0_60` or later installed in your Computer.
- > Having any Java 8 version is not enough.
- This app will not work with earlier versions of Java 8. - -1. Download the latest `addressbook.jar` from the [releases](../../../releases) tab. -2. Copy the file to the folder you want to use as the home folder for your Address Book. -3. Double-click the file to start the app. The GUI should appear in a few seconds. - > - -4. Type the command in the command box and press Enter to execute it.
- e.g. typing **`help`** and pressing Enter will open the help window. -5. Some example commands you can try: - * **`list`** : lists all contacts - * **`add`**` John Doe p/98765432 e/johnd@gmail.com a/John street, block 123, #01-01` : - adds a contact named `John Doe` to the Address Book. - * **`delete`**` 3` : deletes the 3rd contact shown in the current list - * **`exit`** : exits the app -6. Refer to the [Features](#features) section below for details of each command.
- - -## Features - -> **Command Format** -> * Words in `UPPER_CASE` are the parameters. -> * Items in `SQUARE_BRACKETS` are optional. -> * Items with `...` after them can have multiple instances. -> * The order of parameters is fixed. - -#### Viewing help : `help` -Format: `help` - -> Help is also shown if you enter an incorrect command e.g. `abcd` - -#### Adding a person: `add` -Adds a person to the address book
-Format: `add NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` - -> Persons can have any number of tags (including 0) - -Examples: -* `add John Doe p/98765432 e/johnd@gmail.com a/John street, block 123, #01-01` -* `add Betsy Crowe p/1234567 e/betsycrowe@gmail.com a/Newgate Prison t/criminal t/friend` - -#### Listing all persons : `list` -Shows a list of all persons in the address book.
-Format: `list` - -#### Finding all persons containing any keyword in their name: `find` -Finds persons whose names contain any of the given keywords.
-Format: `find KEYWORD [MORE_KEYWORDS]` - -> * The search is case sensitive. e.g `hans` will not match `Hans` -> * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -> * Only the name is searched. -> * Only full words will be matched e.g. `Han` will not match `Hans` -> * Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans` will match `Hans Bo` - -Examples: -* `find John`
- Returns `John Doe` but not `john` -* `find Betsy Tim John`
- Returns Any person having names `Betsy`, `Tim`, or `John` - -#### Deleting a person : `delete` -Deletes the specified person from the address book. Irreversible.
-Format: `delete INDEX` - -> Deletes the person at the specified `INDEX`. - The index refers to the index number shown in the most recent listing.
- The index **must be a positive integer** 1, 2, 3, ... - -Examples: -* `list`
- `delete 2`
- Deletes the 2nd person in the address book. -* `find Betsy`
- `delete 1`
- Deletes the 1st person in the results of the `find` command. - -#### Select a person : `select` -Selects the person identified by the index number used in the last person listing.
-Format: `select INDEX` - -> Selects the person and loads the Google search page the person at the specified `INDEX`. - The index refers to the index number shown in the most recent listing.
- The index **must be a positive integer** 1, 2, 3, ... - -Examples: -* `list`
- `select 2`
- Selects the 2nd person in the address book. -* `find Betsy`
- `select 1`
- Selects the 1st person in the results of the `find` command. - -#### Clearing all entries : `clear` -Clears all entries from the address book.
-Format: `clear` - -#### Exiting the program : `exit` -Exits the program.
-Format: `exit` - -#### Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data.
-There is no need to save manually. - -## FAQ - -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with - the file that contains the data of your previous Address Book folder. - -## Command Summary - -Command | Format --------- | :-------- -Add | `add NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` -Clear | `clear` -Delete | `delete INDEX` -Find | `find KEYWORD [MORE_KEYWORDS]` -List | `list` -Help | `help` -Select | `select INDEX` diff --git a/docs/UsingAppVeyor.adoc b/docs/UsingAppVeyor.adoc new file mode 100644 index 000000000000..175b857ccbde --- /dev/null +++ b/docs/UsingAppVeyor.adoc @@ -0,0 +1,87 @@ += AppVeyor +:imagesDir: images +:stylesDir: stylesheets + +https://www.appveyor.com/[AppVeyor] is a _Continuous Integration_ platform for GitHub projects. It runs its builds on Windows virtual machines. + +AppVeyor can run the project's tests automatically whenever new code is pushed to the repo. This ensures that existing functionality and features have not been broken on Windows by the changes. + +The current AppVeyor setup performs the following things whenever someone pushes code to the repo: + +* Runs the `gradlew.bat headless allTests` command. +* Automatically retries the build up to 3 times if a task fails. + +If you would like to customize your AppVeyor build further, you can learn more about AppVeyor from the https://www.appveyor.com/docs/[AppVeyor Documentation]. + +== Setting up AppVeyor + +. Fork the repo to your own organization. +. Go to https://ci.appveyor.com/, and under `Login`, click on `GitHub` to login with your GitHub account. Enter your GitHub account details if needed. ++ +image:appveyor/login.png[Click on GitHub in the login page] ++ +. After logging in, you will be brought to your projects dashboard. Click on `NEW PROJECT`. ++ +image:appveyor/add-project-1.png[Click on "NEW PROJECT" in the projects dashboard] ++ +. You will be brought to the `Select repository` page. Select `GitHub`. +* On your first usage of AppVeyor, you will need to give AppVeyor authorization to your GitHub account. Click on `Authorize GitHub`. ++ +image:appveyor/add-project-2.png[Click on Authorize GitHub] ++ +* This will bring you to a GitHub page that manages the access of third-party applications to your repositories. ++ +Depending on whether you are the owner of the repository, you can either +grant access: ++ +image:grant_access.png[Grant Access] ++ +Or request access: ++ +image:request_access.png[Request Access] ++ +. AppVeyor will then list the repositories you have access to in your GitHub account. Find the repository you want to set AppVeyor up on, and then click `ADD`. ++ +image:appveyor/add-project-3.png[Click "Add" on the repository you want to set AppVeyor up on] ++ +. AppVeyor will then be activated on that repository. To see the CI in action, push a commit to any branch! +* Go to the repository and see the pushed commit. There should be an icon which will link you to the AppVeyor build: ++ +image:appveyor/ci-pending.png[Commit build] ++ +* As the build is run on a remote machine, we can only examine the logs it produces: ++ +image:appveyor/ci-log.png[AppVeyor build] ++ +. Update the link to the "build status" badge at the top of `README.adoc` to point to the AppVeyor build status of your own repo. +* To find your build status badge URL, first go to your project settings by clicking on the "Settings" icon: ++ +image:appveyor/project-settings-1.png[Click on project settings] ++ +* Then go to the `Badges` section of your project settings by clicking on it: ++ +image:appveyor/project-settings-2.png[Click on "Badges"] ++ +* As AppVeyor does not provide asciidoc code for the badge, we will have to create our own. Start by copying the markdown code provided: ++ +image:appveyor/project-settings-3.png[Copy the markdown code] ++ +The markdown code should be in this format: ++ +---- +[![Build status]()]() +---- ++ +Convert it to the asciidoc format as follows: ++ +---- +[image:[Build status]] +---- ++ +The asciidoc code should look similar to: ++ +---- +https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] +---- ++ +Copy and paste the asciidoc code to your `README.adoc` file. diff --git a/docs/UsingCheckstyle.adoc b/docs/UsingCheckstyle.adoc new file mode 100644 index 000000000000..ba9b76d3918d --- /dev/null +++ b/docs/UsingCheckstyle.adoc @@ -0,0 +1,40 @@ += Using Checkstyle-IDEA +:imagesDir: images +:stylesDir: stylesheets +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] + +== Configuring Checkstyle-IDEA + +. Install the Checkstyle-IDEA plugin by going to `File` > `Settings` (Windows/Linux), or `IntelliJ IDEA` > `Preferences...` (macOS). + +Select `Plugins`, press `Browse Repository`, and find the plugin. + +Restart the IDE to complete the installation. +. Click `File` > `Settings...` > `Other Settings` > `Checkstyle` +. Set `Scan Scope` to `Only Java sources (including tests)`, so that the plugin will run checkstyle for our test source codes as well +. Ensure that the `Checkstyle version` is set to `8.1`. This is the same version that we are using inside Gradle, so that you won't get any errors due to version incompatibility ++ +image::checkstyle-idea-scan-scope.png[width="500"] +. Click the plus sign under `Configuration File` +. Enter an arbitrary description e.g. addressbook +. Select `Use a local Checkstyle file` +. Use the checkstyle configuration file found at `config/checkstyle/checkstyle.xml` +. Click `Next` > `Finish` +. Mark `Active` for the newly imported check configuration ++ +image::checkstyle-idea-configuration.png[width="700"] +. Click `OK` + +== Troubleshooting Checkstyle-IDEA + +**Problem: When importing `checkstyle.xml`, Checkstyle-IDEA plugin complains that `The Checkstyle rules file could not be parsed. ... The file has been blacklisted for 60s.`** + +* Reason: `checkstyle.xml` is written for a particular version, but the plugin was not configured to the correct version. +* Solution: Ensure that you have selected the correct `Checkstyle version`, that matches the version in `build.gradle`, as `checkstyle.xml` is written for Gradle's checkstyle. + +**Problem: After setting up `checkstyle.xml`, Checkstyle-IDEA plugin does not seem to highlight the errors / real-time scanning seems broken.** + +* Reason: The plugin may not immediately run after setting it up. +* Solution: Restart the IDE. diff --git a/docs/UsingGradle.adoc b/docs/UsingGradle.adoc new file mode 100644 index 000000000000..ef377ae5482e --- /dev/null +++ b/docs/UsingGradle.adoc @@ -0,0 +1,106 @@ += Using Gradle +:imagesDir: images +:stylesDir: stylesheets +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] + +https://gradle.org/[Gradle] is a build automation tool. It can automate build-related tasks such as + +* Running tests +* Managing library dependencies +* Analyzing code for style compliance + +The gradle configuration for this project is defined in the _build script_ link:../build.gradle[`build.gradle`]. + +[NOTE] +To learn more about gradle build scripts, refer https://docs.gradle.org/current/userguide/tutorial_using_tasks.html[Build Scripts Basics]. + +== Running Gradle Commands + +To run a Gradle command, open a command window on the project folder and enter the Gradle command. Gradle commands look like this: + +* On Windows: `gradlew ...` e.g. `gradlew clean allTests` +* On Mac/Linux: `./gradlew ...` e.g. +`./gradlew clean allTests` + +[NOTE] +If you do not specify any tasks, Gradlew will run the default tasks `clean` `headless` `allTests` `coverage` + +== Cleaning the Project + +* *`clean`* + +Deletes the files created during the previous build tasks (e.g. files in the `build` folder). e.g. `./gradlew clean` + +[TIP] +*`clean` to force Gradle to execute a task*: + +When running a Gradle task, Gradle will try to figure out if the task needs running at all. If Gradle determines that the output of the task will be same as the previous time, it will not run the task. For example, it will not build the JAR file again if the relevant source files have not changed since the last time the JAR file was built. If we want to force Gradle to run a task, we can combine that task with `clean`. Once the build files have been `clean` ed, Gradle has no way to determine if the output will be same as before, so it will be forced to execute the task. + +== Creating the JAR file + +* *`shadowJar`* + +Creates the `addressbook.jar` file in the `build/jar` folder, _if the current file is outdated_. + +e.g. `./gradlew shadowJar` + +**** +To force Gradle to create the JAR file even if the current one is up-to-date, you can '`clean`' first. + +e.g. `./gradlew clean shadowJar` +**** + +[NOTE] +*Why do we create a fat JAR?* If we package only our own class files into the JAR file, it will not work properly unless the user has all the other JAR files (i.e. third party libraries) our classes depend on, which is rather inconvenient. Therefore, we package all dependencies into a single JAR files, creating what is also known as a _fat_ JAR file. To create a fat JAR file, we use the Gradle plugin https://github.com/johnrengelman/shadow[shadow jar]. + +== Rendering AsciiDoc files + +* **`asciidoctor`** + +Converts AsciiDoc files in `docs` to HTML format. Generated HTML files can be found in `build/docs`. +* **`deployOfflineDocs`** + +Updates the offline user guide, and its associated files, used by the Help window in the application. Deployed HTML files and images can be found in `src/main/resources/docs`. + +== Running the application + +* *`run`* + +Builds and runs the application. +* *`runShadow`* + +Builds the application as a fat JAR, and then runs it. + +== Running code style checks + +* **`checkstyleMain`** + +Runs the code style check for the main code base +* **`checkstyleTest`** + +Runs the code style check for the test code base + +The set of code style rules implemented can be found in `config/checkstyle/checkstyle.xml`. To enable _exceptions_ to code styles, add in the comment `//CODESTYLE.OFF: RuleName` at the start of the section and `//CODESTYLE.ON: RuleName` at the end of the section. + +== Running Tests + +* **`allTests`** + +Runs all tests. +* **`guiTests`** + +Runs all tests in the `guitests` package +* **`nonGuiTests`** + +Runs all non-GUI tests in the `seedu.address` +package +* **`headless`** + +Sets the test mode as _headless_. The mode is effective for that Gradle run only so it should be combined with other test tasks. + +Here are some examples: + +* `./gradlew headless allTests` -- Runs all tests in headless mode +* `./gradlew clean nonGuiTests` -- Cleans the project and runs non-GUI +tests + +== Updating Dependencies + +There is no need to run these Gradle tasks manually as they are called automatically by other relevant Gradle tasks. + +* **`compileJava`** + +Checks whether the project has the required dependencies to compile and run the main program, and download any missing dependencies before compiling the classes. + +See `build.gradle` -> +`allprojects` -> `dependencies` -> `compile` for the list of dependencies required. +* **`compileTestJava`** + +Checks whether the project has the required dependencies to perform testing, and download any missing dependencies before compiling the test classes. + +See `build.gradle` -> `allprojects` -> `dependencies` -> `testCompile` for the list of dependencies required. diff --git a/docs/UsingGradle.md b/docs/UsingGradle.md deleted file mode 100644 index 578c5f8634c2..000000000000 --- a/docs/UsingGradle.md +++ /dev/null @@ -1,85 +0,0 @@ -# Using Gradle - -[Gradle](https://gradle.org/) is a build automation tool. It can automate build-related tasks such as -* Running tests -* Managing library dependencies -* Analyzing code for style compliance - -The gradle configuration for this project is defined in the _build script_ [`build.gradle`](../build.gradle). -> To learn more about gradle build scripts, -refer [Build Scripts Basics](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html). - -## Running Gradle Commands - -To run a Gradle command, open a command window on the project folder and enter the Gradle command. -Gradle commands look like this: -* On Windows :`gradlew ...` e.g. `gradlew clean allTests` -* On Mac/Linux: `./gradlew ...` e.g. `./gradlew clean allTests` - -> If you do not specify any tasks, Gradlew will run the default tasks `clean` `headless` `allTests` `coverage` - -## Cleaning the Project - -* **`clean`**
- Deletes the files created during the previous build tasks (e.g. files in the `build` folder).
- e.g. `./gradlew clean` - - >**Tip `clean` to force Gradle to execute a task**:
- When running a Gradle task, Gradle will try to figure out if the task needs running at all. - If Gradle determines that the output of the task will be same as the previous time, it will not run - the task. For example, it will not build the JAR file again if the relevant source files have not changed - since the last time the JAR file was built. If we want to force Gradle to run a task, we can combine - that task with `clean`. Once the build files have been `clean`ed, Gradle has no way to determine if - the output will be same as before, so it will be forced to execute the task. - -## Creating the JAR file - -* **`shadowJar`**
- Creates the `addressbook.jar` file in the `build/jar` folder, _if the current file is outdated_.
- e.g. `./gradlew shadowJar` - - > To force Gradle to create the JAR file even if the current one is up-to-date, you can '`clean`' first.
- e.g. `./gradlew clean shadowJar` - -**Note: Why do we create a fat JAR?** -If we package only our own class files into the JAR file, it will not work properly unless the user has all the other - JAR files (i.e. third party libraries) our classes depend on, which is rather inconvenient. - Therefore, we package all dependencies into a single JAR files, creating what is also known as a _fat_ JAR file. - To create a fat JAR fil, we use the Gradle plugin [shadow jar](https://github.com/johnrengelman/shadow). - -## Running Tests - -* **`allTests`**
- Runs all tests. - -* **`guiTests`**
- Runs all tests in the `guitests` package - -* **`nonGuiTests`**
- Runs all non-GUI tests in the `seedu.address` package - -* **`headless`**
- Sets the test mode as _headless_. - The mode is effective for that Gradle run only so it should be combined with other test tasks. - -Here are some examples: - -* `./gradlew headless allTests` -- Runs all tests in headless mode -* `./gradlew clean nonGuiTests` -- Cleans the project and runs non-GUI tests - - -## Updating Dependencies - -There is no need to run these Gradle tasks manually as they are called automatically by other -relevant Gradle tasks. - -* **`compileJava`**
- Checks whether the project has the required dependencies to compile and run the main program, and download - any missing dependencies before compiling the classes.
- See `build.gradle` -> `allprojects` -> `dependencies` -> `compile` for the list of dependencies required. - -* **`compileTestJava`**
- Checks whether the project has the required dependencies to perform testing, and download - any missing dependencies before compiling the test classes.
- See `build.gradle` -> `allprojects` -> `dependencies` -> `testCompile` for the list of - dependencies required. diff --git a/docs/UsingTravis.adoc b/docs/UsingTravis.adoc new file mode 100644 index 000000000000..6e4c2882dc50 --- /dev/null +++ b/docs/UsingTravis.adoc @@ -0,0 +1,132 @@ += Travis CI +:imagesDir: images +:stylesDir: stylesheets +ifdef::env-github,env-browser[:outfilesuffix: .adoc] + +https://travis-ci.org/[Travis CI] is a _Continuous Integration_ platform for GitHub projects. + +Travis CI can run the projects' tests automatically whenever new code is pushed to the repo. This ensures that existing functionality and features have not been broken by the changes. + +The current Travis CI set up performs the following things whenever someone push code to the repo: + +* Runs the `./gradlew clean headless allTests coverage coveralls -i` command (see <> for more details on what this command means). +* Automatically retries the build up to 3 times if a task fails. +* Renders documentation from asciidoc to html and automatically publishes them using GitHub Pages. +* Runs additional link:#repository-wide-checks[repository-wide checks]. + +If you would like to customise your travis build further, you can learn more about Travis from https://docs.travis-ci.com/[Travis CI Documentation]. + +== Setting up Travis CI + +. Fork the repo to your own organization. +. Go to https://travis-ci.org/ and click `Sign in with GitHub`, then enter your GitHub account details if needed. ++ +image:signing_in.png[Signing into Travis CI] ++ +. Head to the https://travis-ci.org/profile[Accounts] page, and find the switch for the forked repository. +* If the organization is not shown, click `Review and add` as shown below: ++ +image:review_and_add.png[Review and add] ++ +This should bring you to a GitHub page that manages the access of third-party applications. Depending on whether you are the owner of the repository, you can either grant access ++ +image:grant_access.png[Grant Access] ++ +or request access ++ +image:request_access.png[Request Access] ++ +to Travis CI so that it can access your commits and build your code. +* If repository cannot be found, click `Sync account` +. Activate the switch. ++ +image:flick_repository_switch.png[Activate the switch] ++ +. This repo comes with a link:../.travis.yml[`.travis.yml`] that tells Travis what to do. So there is no need for you to create one yourself. +. To see the CI in action, push a commit to the master branch! +* Go to the repository and see the pushed commit. There should be an icon which will link you to the Travis build. ++ +image:build_pending.png[Commit build] ++ +* As the build is run on a provided remote machine, we can only examine the logs it produces: ++ +image:travis_build.png[Travis build] ++ +. If the build is successful, you should be able to check the coverage details of the tests at http://coveralls.io/[Coveralls] +. Update the link to the 'build status' badge at the top of the `README.adoc` to point to the build status of your own repo. + +== Enabling auto-publishing of documentation + +. Ensure that you have followed the steps above to set up Travis CI. +. On GitHub, create a new user account and give this account collaborator and admin access to the repo. + + Using this account, generate a personal access token https://github.com/settings/tokens/new[here]. ++ +[NOTE] +Personal access tokens are like passwords so make sure you keep them secret! If the personal access token is leaked, please delete it and generate a new one. ++ +[NOTE] +We use a new user account to generate the token for team projects to prevent team members from gaining access to other team members' repos. + +If you are the only one with write access to the repo, you can use your own account to generate the token. ++ +-- +* Add a description for the token. (e.g. `Travis CI - deploy docs to gh-pages`) +* Check the `public_repo` checkbox. +* Click `Generate Token` and copy your new personal access token. +-- +We will use this token to grant Travis access to the repo. ++ +image:generate_token.png[Generate token] + +. Head to the https://travis-ci.org/profile[Accounts] page, and find the switch for the forked repository. ++ +image:flick_repository_switch.png[Activate the switch] ++ +. Click on the settings button next to the switch. In the Environment Variables section, add a new environment variable with ++ +-- +* name: `GITHUB_TOKEN` +* value: personal access token copied in step 1 +* Display value in build log: `OFF` +-- +image:travis_add_token.png[Travis add token] ++ +[NOTE] +*Make sure you set `Display value in build log` to `OFF`.* + +Otherwise, other people will be able to see the personal access token and thus have access this repo. + +Similarly, make sure you *do not print `$GITHUB_TOKEN` to the logs* in Travis scripts as the logs are viewable by the public. + +. Now, whenever there's a new commit to master branch, Travis will push the latest documentation to gh-pages branch. + +**To verify that it works,** + +. Trigger Travis to regenerate documentation. To do so, you need to push a new commit to the master branch of the fork. + + Suggested change: Remove the codacy badge from `README`. +. Wait for Travis CI to finish running the build on your new commit. +. Go to the URL `\https://.github.io/addressbook-level4/`. You should see your `README` file displayed. + +== Repository-wide checks + +In addition to running Gradle checks, we also configure Travis CI to run some repository-wide checks. Unlike the Gradle checks which only cover files used in the build process, these repository-wide checks cover _all_ files in the repository. They check for repository rules which are hard to enforce on development machines such as line ending requirements. + +These checks are implemented as POSIX shell scripts, and thus can only be run on POSIX-compliant operating systems such as macOS and Linux. To run all checks locally on these operating systems, execute the following in the repository root directory: + +[source,shell] +---- +./config/travis/run-checks.sh +---- + +Any warnings or errors will be printed out to the console. + +=== Implementing new checks + +Checks are implemented as executable `check-*` scripts within the `config/travis/` directory. The `run-checks.sh` script will automatically pick up and run files named as such. + +Check scripts should print out errors in the following format: + +.... +SEVERITY:FILENAME:LINE: MESSAGE +.... + +where `SEVERITY` is either `ERROR` or `WARN`, `FILENAME` is the path to the file relative to the current directory, `LINE` is the line of the file where the error occurred and `MESSAGE` is the message explaining the error. + +Check scripts must exit with a non-zero exit code if any errors occur. diff --git a/docs/UsingTravis.md b/docs/UsingTravis.md deleted file mode 100644 index 4844f0682f75..000000000000 --- a/docs/UsingTravis.md +++ /dev/null @@ -1,47 +0,0 @@ -# Travis CI - -[Travis CI](https://travis-ci.org/) is a _Continuous Integration_ platform for GitHub projects. - -Travis CI can run the projects' tests automatically whenever new code is pushed to the repo. -This ensures that existing functionality and features have not been broken by the changes. - -The current Travis CI set up performs the following things whenever someone push code to the repo: - * Runs the `./gradlew clean headless allTests coverage coveralls -i` command - (see [UsingGradle.md](UsingGradle.md) for more details on what this command means). - * Automatically retries the build up to 3 times if a task fails. - -If you would like to customise your travis build further, you can learn more about Travis -from [Travis CI Documentation](https://docs.travis-ci.com/). - -## Setting up Travis CI - -1. Fork the repo to your own organization. -2. Go to https://travis-ci.org/ and click `Sign in with GitHub`, then enter your GitHub account details if needed.
-![Signing into Travis CI](images/signing_in.png) - -3. Head to the [Accounts](https://travis-ci.org/profile) page, and find the switch for the forked repository. - * If the organization is not shown, click `Review and add` as shown below:
- ![Review and add](images/review_and_add.png)
- This should bring you to a GitHub page that manages the access of third-party applications. - Depending on whether you are the owner of the repository, you can either grant access - ![Grant Access](images/grant_access.png)
- or request access
- ![Request Access](images/request_access.png)
- to Travis CI so that it can access your commits and build your code. - - * If repository cannot be found, click `Sync account` -4. Activate the switch.
- ![Activate the switch](images/flick_repository_switch.png) -5. This repo comes with a [`.travis.yml`](.travis.yml) that tells Travis what to do. - So there is no need for you to create one yourself. -6. To see the CI in action, push a commit to the master branch! - * Go to the repository and see the pushed commit. There should be an icon which will link you to the Travis build.
- ![Commit build](images/build_pending.png) - - * As the build is run on a provided remote machine, we can only examine the logs it produces:
- ![Travis build](images/travis_build.png) - -7. If the build is successful, you should be able to check the coverage details of the tests - at [Coveralls](http://coveralls.io/) -8. Update the link to the 'build status' badge at the top of the `README.md` to point to the build status of your - own repo. \ No newline at end of file diff --git a/docs/diagrams/ArchitectureDiagram.pptx b/docs/diagrams/ArchitectureDiagram.pptx new file mode 100644 index 000000000000..b0e5a9d0ff55 Binary files /dev/null and b/docs/diagrams/ArchitectureDiagram.pptx differ diff --git a/docs/diagrams/Diagrams.pptx b/docs/diagrams/Diagrams.pptx deleted file mode 100644 index 3c28abe9c1d3..000000000000 Binary files a/docs/diagrams/Diagrams.pptx and /dev/null differ diff --git a/docs/diagrams/HighLevelSequenceDiagrams.pptx b/docs/diagrams/HighLevelSequenceDiagrams.pptx new file mode 100644 index 000000000000..38332090a79a Binary files /dev/null and b/docs/diagrams/HighLevelSequenceDiagrams.pptx differ diff --git a/docs/diagrams/LogicComponentClassDiagram.pptx b/docs/diagrams/LogicComponentClassDiagram.pptx new file mode 100644 index 000000000000..e38e3de020a3 Binary files /dev/null and b/docs/diagrams/LogicComponentClassDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentCommandClassDiagram.pptx b/docs/diagrams/LogicComponentCommandClassDiagram.pptx new file mode 100644 index 000000000000..50631ba75bf0 Binary files /dev/null and b/docs/diagrams/LogicComponentCommandClassDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentSequenceDiagram.pptx b/docs/diagrams/LogicComponentSequenceDiagram.pptx new file mode 100644 index 000000000000..c5b6d5fad6e3 Binary files /dev/null and b/docs/diagrams/LogicComponentSequenceDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx new file mode 100644 index 000000000000..5e7392e86ec6 Binary files /dev/null and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx new file mode 100644 index 000000000000..be29a9de7ca6 Binary files /dev/null and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx new file mode 100644 index 000000000000..384d0a00e6ea Binary files /dev/null and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoActivityDiagram.pptx b/docs/diagrams/UndoRedoActivityDiagram.pptx new file mode 100644 index 000000000000..9cd6153920f4 Binary files /dev/null and b/docs/diagrams/UndoRedoActivityDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoExecuteUndoStackDiagram.pptx b/docs/diagrams/UndoRedoExecuteUndoStackDiagram.pptx new file mode 100644 index 000000000000..9cea286bfb95 Binary files /dev/null and b/docs/diagrams/UndoRedoExecuteUndoStackDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand1StackDiagram.pptx b/docs/diagrams/UndoRedoNewCommand1StackDiagram.pptx new file mode 100644 index 000000000000..5f6009e0e0e2 Binary files /dev/null and b/docs/diagrams/UndoRedoNewCommand1StackDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand2StackDiagram.pptx b/docs/diagrams/UndoRedoNewCommand2StackDiagram.pptx new file mode 100644 index 000000000000..5d7a9f1546f8 Binary files /dev/null and b/docs/diagrams/UndoRedoNewCommand2StackDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand3StackDiagram.pptx b/docs/diagrams/UndoRedoNewCommand3StackDiagram.pptx new file mode 100644 index 000000000000..be7140637fe9 Binary files /dev/null and b/docs/diagrams/UndoRedoNewCommand3StackDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoSequenceDiagram.pptx b/docs/diagrams/UndoRedoSequenceDiagram.pptx new file mode 100644 index 000000000000..2d313a5d1654 Binary files /dev/null and b/docs/diagrams/UndoRedoSequenceDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoStartingStackDiagram.pptx b/docs/diagrams/UndoRedoStartingStackDiagram.pptx new file mode 100644 index 000000000000..3e9802f24dfe Binary files /dev/null and b/docs/diagrams/UndoRedoStartingStackDiagram.pptx differ diff --git a/docs/images/DeletePersonSdForLogic.png b/docs/images/DeletePersonSdForLogic.png index 6c272fb17af6..0462b9b7be6e 100644 Binary files a/docs/images/DeletePersonSdForLogic.png and b/docs/images/DeletePersonSdForLogic.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index a973d02047a2..e0b17014bb47 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/LogicCommandClassDiagram.png b/docs/images/LogicCommandClassDiagram.png new file mode 100644 index 000000000000..09d61cc1b401 Binary files /dev/null and b/docs/images/LogicCommandClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 8cdf11ec93a1..23d5c799beaa 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PujithaDesiraju.JPG b/docs/images/PujithaDesiraju.JPG new file mode 100644 index 000000000000..c04c26b84436 Binary files /dev/null and b/docs/images/PujithaDesiraju.JPG differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 7121a50a442a..eec0b23b1272 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 459245e267af..369469ef176e 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoActivityDiagram.png b/docs/images/UndoRedoActivityDiagram.png new file mode 100644 index 000000000000..cba227e1fb19 Binary files /dev/null and b/docs/images/UndoRedoActivityDiagram.png differ diff --git a/docs/images/UndoRedoExecuteUndoStackDiagram.png b/docs/images/UndoRedoExecuteUndoStackDiagram.png new file mode 100644 index 000000000000..093e4e4e3028 Binary files /dev/null and b/docs/images/UndoRedoExecuteUndoStackDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand1StackDiagram.png b/docs/images/UndoRedoNewCommand1StackDiagram.png new file mode 100644 index 000000000000..c4d23b665e05 Binary files /dev/null and b/docs/images/UndoRedoNewCommand1StackDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand2StackDiagram.png b/docs/images/UndoRedoNewCommand2StackDiagram.png new file mode 100644 index 000000000000..b98d51d54bb5 Binary files /dev/null and b/docs/images/UndoRedoNewCommand2StackDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand3StackDiagram.png b/docs/images/UndoRedoNewCommand3StackDiagram.png new file mode 100644 index 000000000000..0ad7fc87a2aa Binary files /dev/null and b/docs/images/UndoRedoNewCommand3StackDiagram.png differ diff --git a/docs/images/UndoRedoSequenceDiagram.png b/docs/images/UndoRedoSequenceDiagram.png new file mode 100644 index 000000000000..b69611998e6c Binary files /dev/null and b/docs/images/UndoRedoSequenceDiagram.png differ diff --git a/docs/images/UndoRedoStartingStackDiagram.png b/docs/images/UndoRedoStartingStackDiagram.png new file mode 100644 index 000000000000..9aba88816863 Binary files /dev/null and b/docs/images/UndoRedoStartingStackDiagram.png differ diff --git a/docs/images/appveyor/add-project-1.png b/docs/images/appveyor/add-project-1.png new file mode 100755 index 000000000000..a244bce01461 Binary files /dev/null and b/docs/images/appveyor/add-project-1.png differ diff --git a/docs/images/appveyor/add-project-2.png b/docs/images/appveyor/add-project-2.png new file mode 100755 index 000000000000..7578f8214a8b Binary files /dev/null and b/docs/images/appveyor/add-project-2.png differ diff --git a/docs/images/appveyor/add-project-3.png b/docs/images/appveyor/add-project-3.png new file mode 100755 index 000000000000..c38f521621fd Binary files /dev/null and b/docs/images/appveyor/add-project-3.png differ diff --git a/docs/images/appveyor/ci-log.png b/docs/images/appveyor/ci-log.png new file mode 100644 index 000000000000..40e0f5a52d92 Binary files /dev/null and b/docs/images/appveyor/ci-log.png differ diff --git a/docs/images/appveyor/ci-pending.png b/docs/images/appveyor/ci-pending.png new file mode 100644 index 000000000000..a7d2359994fd Binary files /dev/null and b/docs/images/appveyor/ci-pending.png differ diff --git a/docs/images/appveyor/login.png b/docs/images/appveyor/login.png new file mode 100755 index 000000000000..e5719a546048 Binary files /dev/null and b/docs/images/appveyor/login.png differ diff --git a/docs/images/appveyor/project-settings-1.png b/docs/images/appveyor/project-settings-1.png new file mode 100755 index 000000000000..b772b3f6842d Binary files /dev/null and b/docs/images/appveyor/project-settings-1.png differ diff --git a/docs/images/appveyor/project-settings-2.png b/docs/images/appveyor/project-settings-2.png new file mode 100755 index 000000000000..5ff7f15e46f6 Binary files /dev/null and b/docs/images/appveyor/project-settings-2.png differ diff --git a/docs/images/appveyor/project-settings-3.png b/docs/images/appveyor/project-settings-3.png new file mode 100755 index 000000000000..30485db350da Binary files /dev/null and b/docs/images/appveyor/project-settings-3.png differ diff --git a/docs/images/checkstyle-idea-configuration.png b/docs/images/checkstyle-idea-configuration.png new file mode 100644 index 000000000000..d279d3a4e97c Binary files /dev/null and b/docs/images/checkstyle-idea-configuration.png differ diff --git a/docs/images/checkstyle-idea-scan-scope.png b/docs/images/checkstyle-idea-scan-scope.png new file mode 100644 index 000000000000..39a5b57b1186 Binary files /dev/null and b/docs/images/checkstyle-idea-scan-scope.png differ diff --git a/docs/images/chrome_save_as_pdf.png b/docs/images/chrome_save_as_pdf.png new file mode 100644 index 000000000000..53a1190bd48d Binary files /dev/null and b/docs/images/chrome_save_as_pdf.png differ diff --git a/docs/images/DamithRajapakse.jpg b/docs/images/damithc.jpg similarity index 100% rename from docs/images/DamithRajapakse.jpg rename to docs/images/damithc.jpg diff --git a/docs/images/generate_token.png b/docs/images/generate_token.png new file mode 100644 index 000000000000..aa8cee9f3bee Binary files /dev/null and b/docs/images/generate_token.png differ diff --git a/docs/images/getting-started-ui-result-after.png b/docs/images/getting-started-ui-result-after.png new file mode 100644 index 000000000000..92198515866c Binary files /dev/null and b/docs/images/getting-started-ui-result-after.png differ diff --git a/docs/images/getting-started-ui-result-before.png b/docs/images/getting-started-ui-result-before.png new file mode 100644 index 000000000000..e1c17d85f194 Binary files /dev/null and b/docs/images/getting-started-ui-result-before.png differ diff --git a/docs/images/getting-started-ui-status-after.png b/docs/images/getting-started-ui-status-after.png new file mode 100644 index 000000000000..5963b735b411 Binary files /dev/null and b/docs/images/getting-started-ui-status-after.png differ diff --git a/docs/images/getting-started-ui-status-before.png b/docs/images/getting-started-ui-status-before.png new file mode 100644 index 000000000000..7d3a38e4e45c Binary files /dev/null and b/docs/images/getting-started-ui-status-before.png differ diff --git a/docs/images/getting-started-ui-tag-after.png b/docs/images/getting-started-ui-tag-after.png new file mode 100644 index 000000000000..ffc9e3b9c420 Binary files /dev/null and b/docs/images/getting-started-ui-tag-after.png differ diff --git a/docs/images/getting-started-ui-tag-before.png b/docs/images/getting-started-ui-tag-before.png new file mode 100644 index 000000000000..b00c70b0933b Binary files /dev/null and b/docs/images/getting-started-ui-tag-before.png differ diff --git a/docs/images/github_repo_settings.png b/docs/images/github_repo_settings.png new file mode 100644 index 000000000000..101d6e9d5623 Binary files /dev/null and b/docs/images/github_repo_settings.png differ diff --git a/docs/images/githubicon.png b/docs/images/githubicon.png new file mode 100644 index 000000000000..f13a10c4239f Binary files /dev/null and b/docs/images/githubicon.png differ diff --git a/docs/images/jaryltan.jpg b/docs/images/jaryltan.jpg new file mode 100644 index 000000000000..e86d4913d713 Binary files /dev/null and b/docs/images/jaryltan.jpg differ diff --git a/docs/images/JoshuaLee.jpg b/docs/images/lejolly.jpg similarity index 100% rename from docs/images/JoshuaLee.jpg rename to docs/images/lejolly.jpg diff --git a/docs/images/MartinChoo.jpg b/docs/images/m133225.jpg similarity index 100% rename from docs/images/MartinChoo.jpg rename to docs/images/m133225.jpg diff --git a/docs/images/oscarwang.jpg b/docs/images/oscarwang.jpg new file mode 100755 index 000000000000..7c9f856a9d98 Binary files /dev/null and b/docs/images/oscarwang.jpg differ diff --git a/docs/images/portfolio.png b/docs/images/portfolio.png new file mode 100644 index 000000000000..2914783d9745 Binary files /dev/null and b/docs/images/portfolio.png differ diff --git a/docs/images/rsjunior37.jpg b/docs/images/rsjunior37.jpg new file mode 100644 index 000000000000..f3a5e9cd08a1 Binary files /dev/null and b/docs/images/rsjunior37.jpg differ diff --git a/docs/images/searchbox.PNG b/docs/images/searchbox.PNG new file mode 100644 index 000000000000..1d9461d110ec Binary files /dev/null and b/docs/images/searchbox.PNG differ diff --git a/docs/images/travis_add_token.png b/docs/images/travis_add_token.png new file mode 100644 index 000000000000..06e4dd075faf Binary files /dev/null and b/docs/images/travis_add_token.png differ diff --git a/docs/images/LeowYijin.jpg b/docs/images/yijinl.jpg similarity index 100% rename from docs/images/LeowYijin.jpg rename to docs/images/yijinl.jpg diff --git a/docs/images/YouLiang.jpg b/docs/images/yl_coder.jpg similarity index 100% rename from docs/images/YouLiang.jpg rename to docs/images/yl_coder.jpg diff --git a/docs/index.adoc b/docs/index.adoc new file mode 100644 index 000000000000..a65ae663288f --- /dev/null +++ b/docs/index.adoc @@ -0,0 +1,2 @@ +:stylesDir: stylesheets +include::../README.adoc[] diff --git a/docs/stylesheets/asciidoctor.css b/docs/stylesheets/asciidoctor.css new file mode 100644 index 000000000000..36590bf346cd --- /dev/null +++ b/docs/stylesheets/asciidoctor.css @@ -0,0 +1,407 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ +/* Remove comment around @import statement below when using as a custom stylesheet */ +/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";*/ +article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} +audio,canvas,video{display:inline-block} +audio:not([controls]){display:none;height:0} +[hidden],template{display:none} +script{display:none!important} +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +body{margin:0} +a{background:transparent} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +abbr[title]{border-bottom:1px dotted} +b,strong{font-weight:bold} +dfn{font-style:italic} +hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:monospace;font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} +input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} +input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} +html,body{font-size:100%} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +body{-webkit-font-smoothing:antialiased} +img,object,svg{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +.center{margin-left:auto;margin-right:auto} +.spread{width:100%} +p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6} +.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} +div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} +a{color:#2156a5;text-decoration:underline;line-height:inherit} +a:hover,a:focus{color:#1d4b8f} +a img{border:none} +p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} +p aside{font-size:.875em;line-height:1.35;font-style:italic} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} +h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} +h1{font-size:2.125em} +h2{font-size:1.6875em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} +h4,h5{font-size:1.125em} +h6{font-size:1em} +hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} +em,i{font-style:italic;line-height:inherit} +strong,b{font-weight:bold;line-height:inherit} +small{font-size:60%;line-height:inherit} +code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} +ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} +ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em} +ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} +ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} +ul.square{list-style-type:square} +ul.circle{list-style-type:circle} +ul.disc{list-style-type:disc} +ul.no-bullet{list-style:none} +ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} +dl dt{margin-bottom:.3125em;font-weight:bold} +dl dd{margin-bottom:1.25em} +abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} +abbr{text-transform:none} +blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} +blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} +blockquote cite:before{content:"\2014 \0020"} +blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} +blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} +@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} +h1{font-size:2.75em} +h2{font-size:2.3125em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} +h4{font-size:1.4375em}} +table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} +table thead,table tfoot{background:#f7f8f7;font-weight:bold} +table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} +table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} +table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} +table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} +body{tab-size:4} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} +h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} +.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} +.clearfix:after,.float-group:after{clear:both} +*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} +pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} +.keyseq{color:rgba(51,51,51,.8)} +kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} +.keyseq kbd:first-child{margin-left:0} +.keyseq kbd:last-child{margin-right:0} +.menuseq,.menu{color:rgba(0,0,0,.8)} +b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} +b.button:before{content:"[";padding:0 3px 0 2px} +b.button:after{content:"]";padding:0 2px 0 3px} +p a>code:hover{color:rgba(0,0,0,.9)} +#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} +#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} +#header:after,#content:after,#footnotes:after,#footer:after{clear:both} +#content{margin-top:1.25em} +#content:before{content:none} +#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} +#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8} +#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px} +#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} +#header .details span:first-child{margin-left:-.125em} +#header .details span.email a{color:rgba(0,0,0,.85)} +#header .details br{display:none} +#header .details br+span:before{content:"\00a0\2013\00a0"} +#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} +#header .details br+span#revremark:before{content:"\00a0|\00a0"} +#header #revnumber{text-transform:capitalize} +#header #revnumber:after{content:"\00a0"} +#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} +#toc{border-bottom:1px solid #efefed;padding-bottom:.5em} +#toc>ul{margin-left:.125em} +#toc ul.sectlevel0>li>a{font-style:italic} +#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} +#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} +#toc li{line-height:1.3334;margin-top:.3334em} +#toc a{text-decoration:none} +#toc a:active{text-decoration:underline} +#toctitle{color:#7a2518;font-size:1.2em} +@media only screen and (min-width:768px){#toctitle{font-size:1.375em} +body.toc2{padding-left:15em;padding-right:0} +#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} +#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} +#toc.toc2>ul{font-size:.9em;margin-bottom:0} +#toc.toc2 ul ul{margin-left:0;padding-left:1em} +#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} +body.toc2.toc-right{padding-left:0;padding-right:15em} +body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}} +@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} +#toc.toc2{width:20em} +#toc.toc2 #toctitle{font-size:1.375em} +#toc.toc2>ul{font-size:.95em} +#toc.toc2 ul ul{padding-left:1.25em} +body.toc2.toc-right{padding-left:0;padding-right:20em}} +#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +#content #toc>:first-child{margin-top:0} +#content #toc>:last-child{margin-bottom:0} +#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} +#footer-text{color:rgba(255,255,255,.8);line-height:1.44} +.sect1{padding-bottom:.625em} +@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}} +.sect1+.sect1{border-top:1px solid #efefed} +#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} +#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} +#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} +#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} +#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} +.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} +.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} +table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0} +.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)} +table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit} +.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} +.admonitionblock>table td.icon{text-align:center;width:80px} +.admonitionblock>table td.icon img{max-width:none} +.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} +.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} +.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} +.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} +.exampleblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child{margin-bottom:0} +.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +.sidebarblock>:first-child{margin-top:0} +.sidebarblock>:last-child{margin-bottom:0} +.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} +.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} +.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} +.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em} +.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal} +@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}} +@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}} +.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} +.listingblock pre.highlightjs{padding:0} +.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} +.listingblock pre.prettyprint{border-width:0} +.listingblock>.content{position:relative} +.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} +.listingblock:hover code[data-lang]:before{display:block} +.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} +.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} +table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} +table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45} +table.pyhltable td.code{padding-left:.75em;padding-right:0} +pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} +pre.pygments .lineno{display:inline-block;margin-right:.25em} +table.pyhltable .linenodiv{background:none!important;padding-right:0!important} +.quoteblock{margin:0 1em 1.25em 1.5em;display:table} +.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} +.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} +.quoteblock blockquote{margin:0;padding:0;border:0} +.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} +.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} +.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right} +.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} +.quoteblock .quoteblock blockquote{padding:0 0 0 .75em} +.quoteblock .quoteblock blockquote:before{display:none} +.verseblock{margin:0 1em 1.25em 1em} +.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} +.verseblock pre strong{font-weight:400} +.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} +.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} +.quoteblock .attribution br,.verseblock .attribution br{display:none} +.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} +.quoteblock.abstract{margin:0 0 1.25em 0;display:block} +.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} +.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} +table.tableblock{max-width:100%;border-collapse:separate} +table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0} +table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} +table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} +table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0} +table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} +table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0} +table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0} +table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0} +table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0} +table.frame-all{border-width:1px} +table.frame-sides{border-width:0 1px} +table.frame-topbot{border-width:1px 0} +th.halign-left,td.halign-left{text-align:left} +th.halign-right,td.halign-right{text-align:right} +th.halign-center,td.halign-center{text-align:center} +th.valign-top,td.valign-top{vertical-align:top} +th.valign-bottom,td.valign-bottom{vertical-align:bottom} +th.valign-middle,td.valign-middle{vertical-align:middle} +table thead th,table tfoot th{font-weight:bold} +tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} +tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} +p.tableblock>code:only-child{background:none;padding:0} +p.tableblock{font-size:1em} +td>div.verse{white-space:pre} +ol{margin-left:1.75em} +ul li ol{margin-left:1.5em} +dl dd{margin-left:1.125em} +dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} +ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} +ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} +ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} +ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em} +ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px} +ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} +ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block} +ul.inline>li>*{display:block} +.unstyled dl dt{font-weight:400;font-style:normal} +ol.arabic{list-style-type:decimal} +ol.decimal{list-style-type:decimal-leading-zero} +ol.loweralpha{list-style-type:lower-alpha} +ol.upperalpha{list-style-type:upper-alpha} +ol.lowerroman{list-style-type:lower-roman} +ol.upperroman{list-style-type:upper-roman} +ol.lowergreek{list-style-type:lower-greek} +.hdlist>table,.colist>table{border:0;background:none} +.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} +td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} +td.hdlist1{font-weight:bold;padding-bottom:1.25em} +.literalblock+.colist,.listingblock+.colist{margin-top:-.5em} +.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} +.colist>table tr>td:last-of-type{padding:.25em 0} +.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} +.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} +.imageblock>.title{margin-bottom:0} +.imageblock.thumb,.imageblock.th{border-width:6px} +.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} +.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} +.image.left{margin-right:.625em} +.image.right{margin-left:.625em} +a.image{text-decoration:none;display:inline-block} +a.image object{pointer-events:none} +sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} +sup.footnote a,sup.footnoteref a{text-decoration:none} +sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} +#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} +#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0} +#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;text-indent:-1.05em;margin-bottom:.2em} +#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none} +#footnotes .footnote:last-of-type{margin-bottom:0} +#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} +.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} +.gist .file-data>table td.line-data{width:99%} +div.unbreakable{page-break-inside:avoid} +.big{font-size:larger} +.small{font-size:smaller} +.underline{text-decoration:underline} +.overline{text-decoration:overline} +.line-through{text-decoration:line-through} +.aqua{color:#00bfbf} +.aqua-background{background-color:#00fafa} +.black{color:#000} +.black-background{background-color:#000} +.blue{color:#0000bf} +.blue-background{background-color:#0000fa} +.fuchsia{color:#bf00bf} +.fuchsia-background{background-color:#fa00fa} +.gray{color:#606060} +.gray-background{background-color:#7d7d7d} +.green{color:#006000} +.green-background{background-color:#007d00} +.lime{color:#00bf00} +.lime-background{background-color:#00fa00} +.maroon{color:#600000} +.maroon-background{background-color:#7d0000} +.navy{color:#000060} +.navy-background{background-color:#00007d} +.olive{color:#606000} +.olive-background{background-color:#7d7d00} +.purple{color:#600060} +.purple-background{background-color:#7d007d} +.red{color:#bf0000} +.red-background{background-color:#fa0000} +.silver{color:#909090} +.silver-background{background-color:#bcbcbc} +.teal{color:#006060} +.teal-background{background-color:#007d7d} +.white{color:#bfbfbf} +.white-background{background-color:#fafafa} +.yellow{color:#bfbf00} +.yellow-background{background-color:#fafa00} +span.icon>.fa{cursor:default} +.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} +.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c} +.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} +.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900} +.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} +.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} +.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} +.conum[data-value] *{color:#fff!important} +.conum[data-value]+b{display:none} +.conum[data-value]:after{content:attr(data-value)} +pre .conum[data-value]{position:relative;top:-.125em} +b.conum *{color:inherit!important} +.conum:not([data-value]):empty{display:none} +dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} +h1,h2,p,td.content,span.alt{letter-spacing:-.01em} +p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} +p,blockquote,dt,td.content,span.alt{font-size:1.0625rem} +p{margin-bottom:1.25rem} +.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} +.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} +.print-only{display:none!important} +@media print{@page{margin:1.25cm .75cm} +*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} +a{color:inherit!important;text-decoration:underline!important} +a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} +a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} +abbr[title]:after{content:" (" attr(title) ")"} +pre,blockquote,tr,img,object,svg{page-break-inside:avoid} +thead{display:table-header-group} +svg{max-width:100%} +p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} +h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} +#toc,.sidebarblock,.exampleblock>.content{background:none!important} +#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important} +.sect1{padding-bottom:0!important} +.sect1+.sect1{border:0!important} +#header>h1:first-child{margin-top:1.25rem} +body.book #header{text-align:center} +body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0} +body.book #header .details{border:0!important;display:block;padding:0!important} +body.book #header .details span:first-child{margin-left:0!important} +body.book #header .details br{display:block} +body.book #header .details br+span:before{content:none!important} +body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} +body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} +.listingblock code[data-lang]:before{display:block} +#footer{background:none!important;padding:0 .9375em} +#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em} +.hide-on-print{display:none!important} +.print-only{display:block!important} +.hide-for-print{display:none!important} +.show-for-print{display:inherit!important}} diff --git a/docs/stylesheets/gh-pages.css b/docs/stylesheets/gh-pages.css new file mode 100644 index 000000000000..165471387ae4 --- /dev/null +++ b/docs/stylesheets/gh-pages.css @@ -0,0 +1,19 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "asciidoctor.css"; /* Default asciidoc style framework - important */ + +/* Custom block: details */ + +.sidebarblock.details > .content { + border-left: .25rem solid rgba(0, 0, 0, 0.1); +} + +.sidebarblock.details > .content { + padding-left: .5rem +} + +.sidebarblock.details { + background-color: transparent; + border: none; + padding-bottom: 0; + padding-top: 0; +} diff --git a/docs/team/jaryltan.adoc b/docs/team/jaryltan.adoc new file mode 100644 index 000000000000..39422781f794 --- /dev/null +++ b/docs/team/jaryltan.adoc @@ -0,0 +1,43 @@ += Jaryl Tan - Project Portfolio +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:imagesDir: ../images +:stylesDir: ../stylesheets + +== LISA: Levels Integrated Secretarial Agent +LISA: Levels Integrated Secretarial Agent is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 6 kLoC. + +*Code contributed*: [https://github.com/CS2103AUG2017-T13-B1/main/pull/47/commits[Functional code]] [https://github.com/CS2103AUG2017-T13-B1/main/pull/47/commits/baf693dc2457e0dca1ca7812d9c47cdfea402e6b[Test code]] + +=== Enhancement Added: Improved Tag management, allowing Tags to be added on and deleted without being overwritten + +==== External behavior + +--- +#Start of Extract [from: User Guide]# + +include::../UserGuide.adoc[tag=add] + +#End of Extract# + +--- + +==== Justification + +Tags are an important feature for an addressbook because of how it gives the user the freedom to connect attributes to certain contacts. Overwriting previously saved tags mean a lot of work and unintented mistake as the main use case of an edit function for tags is to add on rather than to replace. + +==== Implementation + +--- +#Start of Extract [from: Developer Guide]# + +include::../DeveloperGuide.adoc[tag=add] + +#End of Extract# + +--- + +=== Enhancement Proposed: + + +=== Other contributions + diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc new file mode 100644 index 000000000000..8ff0f7742885 --- /dev/null +++ b/docs/team/johndoe.adoc @@ -0,0 +1,50 @@ += John Doe - Project Portfolio +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:imagesDir: ../images +:stylesDir: ../stylesheets + +== Project: AddressBook - Level 4 +AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 6 kLoC. + +*Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] {give links to collated code files} + +=== Enhancement Added: Undo/Redo + +==== External behavior + +--- +#Start of Extract [from: User Guide]# + +include::../UserGuide.adoc[tag=undoredo] + +#End of Extract# + +--- + +==== Justification + +{Justify the need for, and the current design (i.e. external behavior) of, the feature} + +==== Implementation + +--- +#Start of Extract [from: Developer Guide]# + +include::../DeveloperGuide.adoc[tag=undoredo] + +#End of Extract# + +--- + +=== Enhancement Proposed: Add command `remark` + +{Explain similar to the Undo/Redo feature above.} + +=== Other contributions + +* Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) +* Wrote additional tests to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) + +== Project: PowerPointLabs + +{Optionally (not graded), you may include other projects in your portfolio.} diff --git a/docs/team/oscarwang.adoc b/docs/team/oscarwang.adoc new file mode 100644 index 000000000000..2d73479760df --- /dev/null +++ b/docs/team/oscarwang.adoc @@ -0,0 +1,43 @@ += Oscar Wang - Project Portfolio +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:imagesDir: ../images +:stylesDir: ../stylesheets + +== LISA: Levels Integrated Secretarial Agent +LISA: Levels Integrated Secretarial Agent is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 6 kLoC. + +*Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] {give links to collated code files} + +=== Enhancement Added: 'add' command with only one compulsory field: NAME + +==== External behavior + +--- +#Start of Extract [from: User Guide]# + +include::../UserGuide.adoc[tag=add] + +#End of Extract# + +--- + +==== Justification + +{Justify the need for, and the current design (i.e. external behavior) of, the feature} + +==== Implementation + +--- +#Start of Extract [from: Developer Guide]# + +include::../DeveloperGuide.adoc[tag=add] + +#End of Extract# + +--- + +=== Enhancement Proposed: + + +=== Other contributions + diff --git a/docs/team/pujithadesiraju.adoc b/docs/team/pujithadesiraju.adoc new file mode 100644 index 000000000000..fe6fbf0018ed --- /dev/null +++ b/docs/team/pujithadesiraju.adoc @@ -0,0 +1,43 @@ += Pujitha Desiraju - Project Portfolio +ifdef::env-github,env-browser[:outfilesuffix: .adoc] +:imagesDir: ../images +:stylesDir: ../stylesheets + +== LISA: Levels Integrated Secretarial Agent +LISA: Levels Integrated Secretarial Agent is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 6 kLoC. + +*Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] {give links to collated code files} + +=== Enhancement Added: Added a new field to the add command : Date of Birth + +==== External behavior + +--- +#Start of Extract [from: User Guide]# + +include::../UserGuide.adoc[tag=add] + +#End of Extract# + +--- + +==== Justification + +{Justify the need for, and the current design (i.e. external behavior) of, the feature} + +==== Implementation + +--- +#Start of Extract [from: Developer Guide]# + +include::../DeveloperGuide.adoc[tag=add] + +#End of Extract# + +--- + +=== Enhancement Proposed: + + +=== Other contributions + diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 36dc72a74b7a..fa0800d55cb9 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -1,6 +1,12 @@ package seedu.address; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + import com.google.common.eventbus.Subscribe; + import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; @@ -10,29 +16,33 @@ import seedu.address.commons.core.Version; import seedu.address.commons.events.ui.ExitAppRequestEvent; import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.ConfigUtil; import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; -import seedu.address.model.*; -import seedu.address.commons.util.ConfigUtil; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.UserPrefs; +import seedu.address.model.util.SampleDataUtil; +import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; +import seedu.address.storage.UserPrefsStorage; +import seedu.address.storage.XmlAddressBookStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Map; -import java.util.Optional; -import java.util.logging.Logger; - /** * The main entry point to the application. */ public class MainApp extends Application { - private static final Logger logger = LogsCenter.getLogger(MainApp.class); - public static final Version VERSION = new Version(1, 0, 0, true); + public static final Version VERSION = new Version(0, 6, 0, true); + + private static final Logger logger = LogsCenter.getLogger(MainApp.class); protected Ui ui; protected Logic logic; @@ -41,7 +51,6 @@ public class MainApp extends Application { protected Config config; protected UserPrefs userPrefs; - public MainApp() {} @Override public void init() throws Exception { @@ -49,40 +58,47 @@ public void init() throws Exception { super.init(); config = initConfig(getApplicationParameter("config")); - storage = new StorageManager(config.getAddressBookFilePath(), config.getUserPrefsFilePath()); - userPrefs = initPrefs(config); + UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); + userPrefs = initPrefs(userPrefsStorage); + AddressBookStorage addressBookStorage = new XmlAddressBookStorage(userPrefs.getAddressBookFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage); initLogging(config); model = initModelManager(storage, userPrefs); - logic = new LogicManager(model, storage); + logic = new LogicManager(model); ui = new UiManager(logic, config, userPrefs); initEventsCenter(); } - private String getApplicationParameter(String parameterName){ + private String getApplicationParameter(String parameterName) { Map applicationParameters = getParameters().getNamed(); return applicationParameters.get(parameterName); } + /** + * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
+ * The data from the sample address book will be used instead if {@code storage}'s address book is not found, + * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + */ private Model initModelManager(Storage storage, UserPrefs userPrefs) { Optional addressBookOptional; ReadOnlyAddressBook initialData; try { addressBookOptional = storage.readAddressBook(); - if(!addressBookOptional.isPresent()){ - logger.info("Data file not found. Will be starting with an empty AddressBook"); + if (!addressBookOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample AddressBook"); } - initialData = addressBookOptional.orElse(new AddressBook()); + initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataConversionException e) { logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); initialData = new AddressBook(); } catch (IOException e) { - logger.warning("Problem while reading from the file. . Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); initialData = new AddressBook(); } @@ -93,13 +109,18 @@ private void initLogging(Config config) { LogsCenter.init(config); } + /** + * Returns a {@code Config} using the file at {@code configFilePath}.
+ * The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead + * if {@code configFilePath} is null. + */ protected Config initConfig(String configFilePath) { Config initializedConfig; String configFilePathUsed; configFilePathUsed = Config.DEFAULT_CONFIG_FILE; - if(configFilePath != null) { + if (configFilePath != null) { logger.info("Custom Config file specified " + configFilePath); configFilePathUsed = configFilePath; } @@ -110,8 +131,8 @@ protected Config initConfig(String configFilePath) { Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); initializedConfig = configOptional.orElse(new Config()); } catch (DataConversionException e) { - logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + - "Using default config properties"); + logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + + "Using default config properties"); initializedConfig = new Config(); } @@ -124,10 +145,13 @@ protected Config initConfig(String configFilePath) { return initializedConfig; } - protected UserPrefs initPrefs(Config config) { - assert config != null; - - String prefsFilePath = config.getUserPrefsFilePath(); + /** + * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, + * or a new {@code UserPrefs} with default configuration if errors occur when + * reading from the file. + */ + protected UserPrefs initPrefs(UserPrefsStorage storage) { + String prefsFilePath = storage.getUserPrefsFilePath(); logger.info("Using prefs file : " + prefsFilePath); UserPrefs initializedPrefs; @@ -135,11 +159,11 @@ protected UserPrefs initPrefs(Config config) { Optional prefsOptional = storage.readUserPrefs(); initializedPrefs = prefsOptional.orElse(new UserPrefs()); } catch (DataConversionException e) { - logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + - "Using default user prefs"); + logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. . Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); initializedPrefs = new UserPrefs(); } diff --git a/src/main/java/seedu/address/commons/core/ComponentManager.java b/src/main/java/seedu/address/commons/core/ComponentManager.java index 4bc8564e5824..05a400773ae8 100644 --- a/src/main/java/seedu/address/commons/core/ComponentManager.java +++ b/src/main/java/seedu/address/commons/core/ComponentManager.java @@ -13,7 +13,7 @@ public abstract class ComponentManager { /** * Uses default {@link EventsCenter} */ - public ComponentManager(){ + public ComponentManager() { this(EventsCenter.getInstance()); } @@ -22,7 +22,7 @@ public ComponentManager(EventsCenter eventsCenter) { eventsCenter.registerHandler(this); } - protected void raise(BaseEvent event){ + protected void raise(BaseEvent event) { eventsCenter.post(event); } } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index 6441c9ef20f4..8f4d737d0e24 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/address/commons/core/Config.java @@ -14,12 +14,6 @@ public class Config { private String appTitle = "Address App"; private Level logLevel = Level.INFO; private String userPrefsFilePath = "preferences.json"; - private String addressBookFilePath = "data/addressbook.xml"; - private String addressBookName = "MyAddressBook"; - - - public Config() { - } public String getAppTitle() { return appTitle; @@ -45,54 +39,33 @@ public void setUserPrefsFilePath(String userPrefsFilePath) { this.userPrefsFilePath = userPrefsFilePath; } - public String getAddressBookFilePath() { - return addressBookFilePath; - } - - public void setAddressBookFilePath(String addressBookFilePath) { - this.addressBookFilePath = addressBookFilePath; - } - - public String getAddressBookName() { - return addressBookName; - } - - public void setAddressBookName(String addressBookName) { - this.addressBookName = addressBookName; - } - - @Override public boolean equals(Object other) { - if (other == this){ + if (other == this) { return true; } - if (!(other instanceof Config)){ //this handles null as well. + if (!(other instanceof Config)) { //this handles null as well. return false; } - Config o = (Config)other; + Config o = (Config) other; return Objects.equals(appTitle, o.appTitle) && Objects.equals(logLevel, o.logLevel) - && Objects.equals(userPrefsFilePath, o.userPrefsFilePath) - && Objects.equals(addressBookFilePath, o.addressBookFilePath) - && Objects.equals(addressBookName, o.addressBookName); + && Objects.equals(userPrefsFilePath, o.userPrefsFilePath); } @Override public int hashCode() { - return Objects.hash(appTitle, logLevel, userPrefsFilePath, addressBookFilePath, addressBookName); + return Objects.hash(appTitle, logLevel, userPrefsFilePath); } @Override - public String toString(){ + public String toString() { StringBuilder sb = new StringBuilder(); sb.append("App title : " + appTitle); sb.append("\nCurrent log level : " + logLevel); sb.append("\nPreference file Location : " + userPrefsFilePath); - sb.append("\nLocal data file location : " + addressBookFilePath); - sb.append("\nAddressBook name : " + addressBookName); return sb.toString(); } diff --git a/src/main/java/seedu/address/commons/core/EventsCenter.java b/src/main/java/seedu/address/commons/core/EventsCenter.java index 9652cd5c227b..799b976f7eb7 100644 --- a/src/main/java/seedu/address/commons/core/EventsCenter.java +++ b/src/main/java/seedu/address/commons/core/EventsCenter.java @@ -1,17 +1,22 @@ package seedu.address.commons.core; +import java.util.logging.Logger; + import com.google.common.eventbus.EventBus; -import seedu.address.commons.events.BaseEvent; -import java.util.logging.Logger; +import seedu.address.commons.events.BaseEvent; /** * Manages the event dispatching of the app. */ public class EventsCenter { private static final Logger logger = LogsCenter.getLogger(EventsCenter.class); - private final EventBus eventBus; private static EventsCenter instance; + private final EventBus eventBus; + + private EventsCenter() { + eventBus = new EventBus(); + } public static EventsCenter getInstance() { if (instance == null) { @@ -24,13 +29,8 @@ public static void clearSubscribers() { instance = null; } - private EventsCenter() { - eventBus = new EventBus(); - } - - public EventsCenter registerHandler(Object handler) { + public void registerHandler(Object handler) { eventBus.register(handler); - return this; } /** diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index e157ac8b8679..846d714375e4 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -1,6 +1,6 @@ package seedu.address.commons.core; -import java.awt.*; +import java.awt.Point; import java.io.Serializable; import java.util.Objects; @@ -42,14 +42,14 @@ public Point getWindowCoordinates() { @Override public boolean equals(Object other) { - if (other == this){ + if (other == this) { return true; } - if (!(other instanceof GuiSettings)){ //this handles null as well. + if (!(other instanceof GuiSettings)) { //this handles null as well. return false; } - GuiSettings o = (GuiSettings)other; + GuiSettings o = (GuiSettings) other; return Objects.equals(windowWidth, o.windowWidth) && Objects.equals(windowHeight, o.windowHeight) @@ -63,7 +63,7 @@ public int hashCode() { } @Override - public String toString(){ + public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Width : " + windowWidth + "\n"); sb.append("Height : " + windowHeight + "\n"); diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 17939bab4975..46e4c3aac468 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -1,9 +1,14 @@ package seedu.address.commons.core; -import seedu.address.commons.events.BaseEvent; - import java.io.IOException; -import java.util.logging.*; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +import seedu.address.commons.events.BaseEvent; /** * Configures and manages loggers and handlers, including their logging level @@ -24,8 +29,8 @@ public class LogsCenter { /** * Initializes with a custom log level (specified in the {@code config} object) * Loggers obtained *AFTER* this initialization will have their logging level changed
- * Logging levels for existing loggers will only be updated if the logger with the same name is requested again - * from the LogsCenter. + * Logging levels for existing loggers will only be updated if the logger with the same name + * is requested again from the LogsCenter. */ public static void init(Config config) { currentLogLevel = config.getLogLevel(); @@ -33,7 +38,7 @@ public static void init(Config config) { } /** - * Creates a logger with the given name the given name. + * Creates a logger with the given name. */ public static Logger getLogger(String name) { Logger logger = Logger.getLogger(name); @@ -46,11 +51,30 @@ public static Logger getLogger(String name) { return Logger.getLogger(name); } + /** + * Creates a Logger for the given class name. + */ + public static Logger getLogger(Class clazz) { + if (clazz == null) { + return Logger.getLogger(""); + } + return getLogger(clazz.getSimpleName()); + } + + /** + * Adds the {@code consoleHandler} to the {@code logger}.
+ * Creates the {@code consoleHandler} if it is null. + */ private static void addConsoleHandler(Logger logger) { - if (consoleHandler == null) consoleHandler = createConsoleHandler(); + if (consoleHandler == null) { + consoleHandler = createConsoleHandler(); + } logger.addHandler(consoleHandler); } + /** + * Remove all the handlers from {@code logger}. + */ private static void removeHandlers(Logger logger) { Handler[] handlers = logger.getHandlers(); for (Handler handler : handlers) { @@ -58,15 +82,25 @@ private static void removeHandlers(Logger logger) { } } + /** + * Adds the {@code fileHandler} to the {@code logger}.
+ * Creates {@code fileHandler} if it is null. + */ private static void addFileHandler(Logger logger) { try { - if (fileHandler == null) fileHandler = createFileHandler(); + if (fileHandler == null) { + fileHandler = createFileHandler(); + } logger.addHandler(fileHandler); } catch (IOException e) { logger.warning("Error adding file handler for logger."); } } + /** + * Creates a {@code FileHandler} for the log file. + * @throws IOException if there are problems opening the file. + */ private static FileHandler createFileHandler() throws IOException { FileHandler fileHandler = new FileHandler(LOG_FILE, MAX_FILE_SIZE_IN_BYTES, MAX_FILE_COUNT, true); fileHandler.setFormatter(new SimpleFormatter()); @@ -80,14 +114,6 @@ private static ConsoleHandler createConsoleHandler() { return consoleHandler; } - /** - * Creates a Logger for the given class name. - */ - public static Logger getLogger(Class clazz) { - if (clazz == null) return Logger.getLogger(""); - return getLogger(clazz.getSimpleName()); - } - /** * Decorates the given string to create a log message suitable for logging event handling methods. */ @@ -99,6 +125,6 @@ public static String getEventHandlingLogMessage(BaseEvent e, String message) { * @see #getEventHandlingLogMessage(BaseEvent, String) */ public static String getEventHandlingLogMessage(BaseEvent e) { - return getEventHandlingLogMessage(e,""); + return getEventHandlingLogMessage(e, ""); } } diff --git a/src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java b/src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java deleted file mode 100644 index 5c25d8647a8d..000000000000 --- a/src/main/java/seedu/address/commons/core/UnmodifiableObservableList.java +++ /dev/null @@ -1,311 +0,0 @@ -package seedu.address.commons.core; - -import javafx.beans.InvalidationListener; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import javafx.collections.transformation.SortedList; - -import java.text.Collator; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -/** - * Unmodifiable view of an observable list - */ -public class UnmodifiableObservableList implements ObservableList { - - public static final String MUTATION_OP_EXCEPTION_MESSAGE = "Attempted to modify an unmodifiable view"; - - private final ObservableList backingList; - - public UnmodifiableObservableList(ObservableList backingList) { - if (backingList == null) { - throw new NullPointerException(); - } - this.backingList = backingList; - } - - @Override - public final void addListener(ListChangeListener listener) { - backingList.addListener(listener); - } - - @Override - public final void removeListener(ListChangeListener listener) { - backingList.removeListener(listener); - } - - @Override - public final void addListener(InvalidationListener listener) { - backingList.addListener(listener); - } - - @Override - public final void removeListener(InvalidationListener listener) { - backingList.removeListener(listener); - } - - @Override - public final boolean addAll(Object... elements) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean setAll(Object... elements) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean setAll(Collection col) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean removeAll(Object... elements) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean retainAll(Object... elements) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void remove(int from, int to) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - - @Override - public final FilteredList filtered(Predicate predicate) { - return new FilteredList<>(this, predicate); - } - - @Override - public final SortedList sorted(Comparator comparator) { - return new SortedList<>(this, comparator); - } - - @SuppressWarnings("unchecked") - @Override - public final SortedList sorted() { - return sorted(Comparator.nullsFirst((o1, o2) -> { - if (o1 instanceof Comparable) { - return ((Comparable) o1).compareTo(o2); - } - return Collator.getInstance().compare(o1.toString(), o2.toString()); - })); - } - - @Override - public final int size() { - return backingList.size(); - } - - @Override - public final boolean isEmpty() { - return backingList.isEmpty(); - } - - @Override - public final boolean contains(Object o) { - return backingList.contains(o); - } - - @Override - public final Iterator iterator() { - return new Iterator() { - private final Iterator i = backingList.iterator(); - - public final boolean hasNext() { - return i.hasNext(); - } - public final E next() { - return i.next(); - } - public final void remove() { - throw new UnsupportedOperationException(); - } - @Override - public final void forEachRemaining(Consumer action) { - // Use backing collection version - i.forEachRemaining(action); - } - }; - } - - @Override - public final Object[] toArray() { - return backingList.toArray(); - } - - @Override - public final T[] toArray(T[] a) { - return backingList.toArray(a); - } - - @Override - public final boolean add(E o) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean remove(Object o) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean containsAll(Collection c) { - return backingList.containsAll(c); - } - - @Override - public final boolean addAll(Collection c) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean addAll(int index, Collection c) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean removeAll(Collection c) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final boolean retainAll(Collection c) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void replaceAll(UnaryOperator operator) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void sort(Comparator c) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void clear() { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - - @Override - public final boolean equals(Object o) { - return o == this || backingList.equals(o); - } - - @Override - public final int hashCode() { - return backingList.hashCode(); - } - - - @Override - public final E get(int index) { - return backingList.get(index); - } - - @SuppressWarnings("unchecked") - @Override - public final Object set(int index, Object element) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void add(int index, Object element) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final E remove(int index) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final int indexOf(Object o) { - return backingList.indexOf(o); - } - - @Override - public final int lastIndexOf(Object o) { - return backingList.lastIndexOf(o); - } - - @Override - public final ListIterator listIterator() { - return listIterator(0); - } - - @Override - public final ListIterator listIterator(int index) { - return new ListIterator() { - private final ListIterator i = backingList.listIterator(index); - - public final boolean hasNext() { - return i.hasNext(); - } - public final E next() { - return i.next(); - } - public final boolean hasPrevious() { - return i.hasPrevious(); - } - public final E previous() { - return i.previous(); - } - public final int nextIndex() { - return i.nextIndex(); - } - public final int previousIndex() { - return i.previousIndex(); - } - - public final void remove() { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - public final void set(E e) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - public final void add(E e) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @Override - public final void forEachRemaining(Consumer action) { - i.forEachRemaining(action); - } - }; - } - - @Override - public final List subList(int fromIndex, int toIndex) { - return Collections.unmodifiableList(backingList.subList(fromIndex, toIndex)); - } - - @Override - public final boolean removeIf(Predicate filter) { - throw new UnsupportedOperationException(MUTATION_OP_EXCEPTION_MESSAGE); - } - - @SuppressWarnings("unchecked") - @Override - public final Stream stream() { - return (Stream) backingList.stream(); - } - - @Override - public final void forEach(Consumer action) { - backingList.forEach(action); - } - -} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/address/commons/core/Version.java index 7ecb85b18f82..e8fe0d3e6299 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/address/commons/core/Version.java @@ -1,11 +1,11 @@ package seedu.address.commons.core; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Represents a version with major, minor and patch number */ @@ -71,11 +71,22 @@ public String toString() { @Override public int compareTo(Version other) { - return this.major != other.major ? this.major - other.major : - this.minor != other.minor ? this.minor - other.minor : - this.patch != other.patch ? this.patch - other.patch : - this.isEarlyAccess == other.isEarlyAccess() ? 0 : - this.isEarlyAccess ? -1 : 1; + if (this.major != other.major) { + return this.major - other.major; + } + if (this.minor != other.minor) { + return this.minor - other.minor; + } + if (this.patch != other.patch) { + return this.patch - other.patch; + } + if (this.isEarlyAccess == other.isEarlyAccess()) { + return 0; + } + if (this.isEarlyAccess) { + return -1; + } + return 1; } @Override diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java new file mode 100644 index 000000000000..fd119bc926fd --- /dev/null +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -0,0 +1,54 @@ +package seedu.address.commons.core.index; + +/** + * Represents a zero-based or one-based index. + * + * {@code Index} should be used right from the start (when parsing in a new user input), so that if the current + * component wants to communicate with another component, it can send an {@code Index} to avoid having to know what + * base the other component is using for its index. However, after receiving the {@code Index}, that component can + * convert it back to an int if the index will not be passed to a different component again. + */ +public class Index { + private int zeroBasedIndex; + + /** + * Index can only be created by calling {@link Index#fromZeroBased(int)} or + * {@link Index#fromOneBased(int)}. + */ + private Index(int zeroBasedIndex) { + if (zeroBasedIndex < 0) { + throw new IndexOutOfBoundsException(); + } + + this.zeroBasedIndex = zeroBasedIndex; + } + + public int getZeroBased() { + return zeroBasedIndex; + } + + public int getOneBased() { + return zeroBasedIndex + 1; + } + + /** + * Creates a new {@code Index} using a zero-based index. + */ + public static Index fromZeroBased(int zeroBasedIndex) { + return new Index(zeroBasedIndex); + } + + /** + * Creates a new {@code Index} using a one-based index. + */ + public static Index fromOneBased(int oneBasedIndex) { + return new Index(oneBasedIndex - 1); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Index // instanceof handles nulls + && this.zeroBasedIndex == ((Index) other).zeroBasedIndex); // state check + } +} diff --git a/src/main/java/seedu/address/commons/events/BaseEvent.java b/src/main/java/seedu/address/commons/events/BaseEvent.java index 723a9c69fbd5..85e71cbb6b62 100644 --- a/src/main/java/seedu/address/commons/events/BaseEvent.java +++ b/src/main/java/seedu/address/commons/events/BaseEvent.java @@ -1,12 +1,15 @@ package seedu.address.commons.events; +/** + * The base class for all event classes. + */ public abstract class BaseEvent { /** * All Events should have a clear unambiguous custom toString message so that feedback message creation * stays consistent and reusable. * - * For example the event manager post method will call any posted event's toString and print it in the console. + * For example, the event manager post method will call any posted event's toString and print it in the console. */ public abstract String toString(); diff --git a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java index 347a8359e0d5..7db9b5c48ed6 100644 --- a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java +++ b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java @@ -8,7 +8,7 @@ public class AddressBookChangedEvent extends BaseEvent { public final ReadOnlyAddressBook data; - public AddressBookChangedEvent(ReadOnlyAddressBook data){ + public AddressBookChangedEvent(ReadOnlyAddressBook data) { this.data = data; } diff --git a/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java b/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java index f0a0640ee523..7096107d8adf 100644 --- a/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java +++ b/src/main/java/seedu/address/commons/events/storage/DataSavingExceptionEvent.java @@ -7,14 +7,14 @@ */ public class DataSavingExceptionEvent extends BaseEvent { - public Exception exception; + public final Exception exception; public DataSavingExceptionEvent(Exception exception) { this.exception = exception; } @Override - public String toString(){ + public String toString() { return exception.toString(); } diff --git a/src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java b/src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java deleted file mode 100644 index 991f7ae9fa25..000000000000 --- a/src/main/java/seedu/address/commons/events/ui/IncorrectCommandAttemptedEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package seedu.address.commons.events.ui; - -import seedu.address.commons.events.BaseEvent; -import seedu.address.logic.commands.Command; - -/** - * Indicates an attempt to execute an incorrect command - */ -public class IncorrectCommandAttemptedEvent extends BaseEvent { - - public IncorrectCommandAttemptedEvent(Command command) {} - - @Override - public String toString() { - return this.getClass().getSimpleName(); - } - -} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java index 0580d27aecf5..4fc32183f074 100644 --- a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java +++ b/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java @@ -1,5 +1,6 @@ package seedu.address.commons.events.ui; +import seedu.address.commons.core.index.Index; import seedu.address.commons.events.BaseEvent; /** @@ -9,8 +10,8 @@ public class JumpToListRequestEvent extends BaseEvent { public final int targetIndex; - public JumpToListRequestEvent(int targetIndex) { - this.targetIndex = targetIndex; + public JumpToListRequestEvent(Index targetIndex) { + this.targetIndex = targetIndex.getZeroBased(); } @Override diff --git a/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java new file mode 100644 index 000000000000..ff6bfe3b43ae --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java @@ -0,0 +1,23 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * Indicates that a new result is available. + */ +public class NewResultAvailableEvent extends BaseEvent { + + public final String message; + public final boolean isError; + + public NewResultAvailableEvent(String message, boolean isError) { + this.message = message; + this.isError = isError; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java index 95377b326fa6..56c1c9d987f1 100644 --- a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java +++ b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java @@ -1,7 +1,7 @@ package seedu.address.commons.events.ui; import seedu.address.commons.events.BaseEvent; -import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.ui.PersonCard; /** * Represents a selection change in the Person List Panel @@ -9,9 +9,9 @@ public class PersonPanelSelectionChangedEvent extends BaseEvent { - private final ReadOnlyPerson newSelection; + private final PersonCard newSelection; - public PersonPanelSelectionChangedEvent(ReadOnlyPerson newSelection){ + public PersonPanelSelectionChangedEvent(PersonCard newSelection) { this.newSelection = newSelection; } @@ -20,7 +20,7 @@ public String toString() { return this.getClass().getSimpleName(); } - public ReadOnlyPerson getNewSelection() { + public PersonCard getNewSelection() { return newSelection; } } diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java index a473b43bd86f..19124db485c9 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java @@ -10,4 +10,12 @@ public class IllegalValueException extends Exception { public IllegalValueException(String message) { super(message); } + + /** + * @param message should contain relevant information on the failed constraint(s) + * @param cause of the main exception + */ + public IllegalValueException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/seedu/address/commons/util/AppUtil.java index 649cc19aaeda..da90201dfd64 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/seedu/address/commons/util/AppUtil.java @@ -1,5 +1,7 @@ package seedu.address.commons.util; +import static java.util.Objects.requireNonNull; + import javafx.scene.image.Image; import seedu.address.MainApp; @@ -9,8 +11,29 @@ public class AppUtil { public static Image getImage(String imagePath) { - assert imagePath != null; + requireNonNull(imagePath); return new Image(MainApp.class.getResourceAsStream(imagePath)); } + /** + * Checks that {@code condition} is true. Used for validating arguments to methods. + * + * @throws IllegalArgumentException if {@code condition} is false. + */ + public static void checkArgument(Boolean condition) { + if (!condition) { + throw new IllegalArgumentException(); + } + } + + /** + * Checks that {@code condition} is true. Used for validating arguments to methods. + * + * @throws IllegalArgumentException with {@code errorMessage} if {@code condition} is false. + */ + public static void checkArgument(Boolean condition, String errorMessage) { + if (!condition) { + throw new IllegalArgumentException(errorMessage); + } + } } diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/address/commons/util/CollectionUtil.java index fde8394f31e5..52d209e778dd 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/address/commons/util/CollectionUtil.java @@ -1,34 +1,38 @@ package seedu.address.commons.util; +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; /** * Utility methods related to Collections */ public class CollectionUtil { + /** @see #requireAllNonNull(Collection) */ + public static void requireAllNonNull(Object... items) { + requireNonNull(items); + Stream.of(items).forEach(Objects::requireNonNull); + } + /** - * Returns true if any of the given items are null. + * Throws NullPointerException if {@code items} or any element of {@code items} is null. */ - public static boolean isAnyNull(Object... items) { - for (Object item : items) { - if (item == null) { - return true; - } - } - return false; + public static void requireAllNonNull(Collection items) { + requireNonNull(items); + items.forEach(Objects::requireNonNull); } - - /** - * Throws an assertion error if the collection or any item in it is null. + * Returns true if {@code items} contain any elements that are non-null. */ - public static void assertNoNullElements(Collection items) { - assert items != null; - assert !isAnyNull(items); + public static boolean isAnyNonNull(Object... items) { + return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); } /** diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/seedu/address/commons/util/ConfigUtil.java index af42e03df06c..7eb770dba3e3 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/seedu/address/commons/util/ConfigUtil.java @@ -1,62 +1,22 @@ package seedu.address.commons.util; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; - -import java.io.File; import java.io.IOException; import java.util.Optional; -import java.util.logging.Logger; + +import seedu.address.commons.core.Config; +import seedu.address.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. */ public class ConfigUtil { - private static final Logger logger = LogsCenter.getLogger(ConfigUtil.class); - - /** - * Returns the Config object from the given file or {@code Optional.empty()} object if the file is not found. - * If any values are missing from the file, default values will be used, as long as the file is a valid json file. - * @param configFilePath cannot be null. - * @throws DataConversionException if the file format is not as expected. - */ public static Optional readConfig(String configFilePath) throws DataConversionException { - - assert configFilePath != null; - - File configFile = new File(configFilePath); - - if (!configFile.exists()) { - logger.info("Config file " + configFile + " not found"); - return Optional.empty(); - } - - Config config; - - try { - config = FileUtil.deserializeObjectFromJsonFile(configFile, Config.class); - } catch (IOException e) { - logger.warning("Error reading from config file " + configFile + ": " + e); - throw new DataConversionException(e); - } - - return Optional.of(config); + return JsonUtil.readJsonFile(configFilePath, Config.class); } - /** - * Saves the Config object to the specified file. - * Overwrites existing file if it exists, creates a new file if it doesn't. - * @param config cannot be null - * @param configFilePath cannot be null - * @throws IOException if there was an error during writing to the file - */ public static void saveConfig(Config config, String configFilePath) throws IOException { - assert config != null; - assert configFilePath != null; - - FileUtil.serializeObjectToJsonFile(new File(configFilePath), config); + JsonUtil.saveJsonFile(config, configFilePath); } } diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index ca8221250de4..73575030d7dd 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -1,19 +1,26 @@ package seedu.address.commons.util; +import static seedu.address.commons.util.AppUtil.checkArgument; + import java.io.File; import java.io.IOException; import java.nio.file.Files; /** - * Writes and reads file + * Writes and reads files */ public class FileUtil { + private static final String CHARSET = "UTF-8"; public static boolean isFileExists(File file) { return file.exists() && file.isFile(); } + /** + * Creates a file if it does not exist along with its missing parent directories. + * @throws IOException if the file or directory cannot be created. + */ public static void createIfMissing(File file) throws IOException { if (!isFileExists(file)) { createFile(file); @@ -79,17 +86,8 @@ public static void writeToFile(File file, String content) throws IOException { * @return {@code pathWithForwardSlash} but '/' replaced with {@code File.separator} */ public static String getPath(String pathWithForwardSlash) { - assert pathWithForwardSlash != null; - assert pathWithForwardSlash.contains("/"); + checkArgument(pathWithForwardSlash.contains("/")); return pathWithForwardSlash.replace("/", File.separator); } - public static void serializeObjectToJsonFile(File jsonFile, T objectToSerialize) throws IOException { - FileUtil.writeToFile(jsonFile, JsonUtil.toJsonString(objectToSerialize)); - } - - public static T deserializeObjectFromJsonFile(File jsonFile, Class classOfObjectToDeserialize) - throws IOException { - return JsonUtil.fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); - } } diff --git a/src/main/java/seedu/address/commons/util/FxViewUtil.java b/src/main/java/seedu/address/commons/util/FxViewUtil.java index 900efa6bf5c3..a949284ba685 100644 --- a/src/main/java/seedu/address/commons/util/FxViewUtil.java +++ b/src/main/java/seedu/address/commons/util/FxViewUtil.java @@ -1,17 +1,18 @@ package seedu.address.commons.util; -import javafx.scene.Node; -import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; /** * Contains utility methods for JavaFX views */ public class FxViewUtil { - public static void applyAnchorBoundaryParameters(Node node, double left, double right, double top, double bottom) { - AnchorPane.setBottomAnchor(node, bottom); - AnchorPane.setLeftAnchor(node, left); - AnchorPane.setRightAnchor(node, right); - AnchorPane.setTopAnchor(node, top); + /** + * Sets the given image as the icon for the given stage. + * @param iconSource e.g. {@code "/images/help_icon.png"} + */ + public static void setStageIcon(Stage stage, String iconSource) { + stage.getIcons().setAll(AppUtil.getImage(iconSource)); } + } diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/address/commons/util/JsonUtil.java index 80b67de5b7e8..1c629b0d4a16 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/address/commons/util/JsonUtil.java @@ -1,5 +1,13 @@ package seedu.address.commons.util; +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; @@ -11,41 +19,15 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import java.io.IOException; -import java.util.logging.Level; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa */ public class JsonUtil { - private static class LevelDeserializer extends FromStringDeserializer { - - protected LevelDeserializer(Class vc) { - super(vc); - } - - @Override - protected Level _deserialize(String value, DeserializationContext ctxt) throws IOException { - return getLoggingLevel(value); - } - - /** - * Gets the logging level that matches loggingLevelString - *

- * Returns null if there are no matches - * - * @param loggingLevelString - * @return - */ - private Level getLoggingLevel(String loggingLevelString) { - return Level.parse(loggingLevelString); - } - @Override - public Class handledType() { - return Level.class; - } - } + private static final Logger logger = LogsCenter.getLogger(JsonUtil.class); private static ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules() .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) @@ -56,6 +38,59 @@ public Class handledType() { .addSerializer(Level.class, new ToStringSerializer()) .addDeserializer(Level.class, new LevelDeserializer(Level.class))); + static void serializeObjectToJsonFile(File jsonFile, T objectToSerialize) throws IOException { + FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize)); + } + + static T deserializeObjectFromJsonFile(File jsonFile, Class classOfObjectToDeserialize) + throws IOException { + return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); + } + + /** + * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found. + * If any values are missing from the file, default values will be used, as long as the file is a valid json file. + * @param filePath cannot be null. + * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here. + * @throws DataConversionException if the file format is not as expected. + */ + public static Optional readJsonFile( + String filePath, Class classOfObjectToDeserialize) throws DataConversionException { + requireNonNull(filePath); + File file = new File(filePath); + + if (!file.exists()) { + logger.info("Json file " + file + " not found"); + return Optional.empty(); + } + + T jsonFile; + + try { + jsonFile = deserializeObjectFromJsonFile(file, classOfObjectToDeserialize); + } catch (IOException e) { + logger.warning("Error reading from jsonFile file " + file + ": " + e); + throw new DataConversionException(e); + } + + return Optional.of(jsonFile); + } + + /** + * Saves the Json object to the specified file. + * Overwrites existing file if it exists, creates a new file if it doesn't. + * @param jsonFile cannot be null + * @param filePath cannot be null + * @throws IOException if there was an error during writing to the file + */ + public static void saveJsonFile(T jsonFile, String filePath) throws IOException { + requireNonNull(filePath); + requireNonNull(jsonFile); + + serializeObjectToJsonFile(new File(filePath), jsonFile); + } + + /** * Converts a given string representation of a JSON data to instance of a class * @param The generic type to create an instance of @@ -75,4 +110,34 @@ public static String toJsonString(T instance) throws JsonProcessingException return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance); } + /** + * Contains methods that retrieve logging level from serialized string. + */ + private static class LevelDeserializer extends FromStringDeserializer { + + protected LevelDeserializer(Class vc) { + super(vc); + } + + @Override + protected Level _deserialize(String value, DeserializationContext ctxt) throws IOException { + return getLoggingLevel(value); + } + + /** + * Gets the logging level that matches loggingLevelString + *

+ * Returns null if there are no matches + * + */ + private Level getLoggingLevel(String loggingLevelString) { + return Level.parse(loggingLevelString); + } + + @Override + public Class handledType() { + return Level.class; + } + } + } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 2e94740456a6..6e403c17c96e 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -1,36 +1,71 @@ package seedu.address.commons.util; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + import java.io.PrintWriter; import java.io.StringWriter; -import java.util.Arrays; -import java.util.List; /** * Helper functions for handling strings. */ public class StringUtil { - public static boolean containsIgnoreCase(String source, String query) { - String[] split = source.toLowerCase().split("\\s+"); - List strings = Arrays.asList(split); - return strings.stream().filter(s -> s.equals(query.toLowerCase())).count() > 0; + + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case, but a full word match is required. + *
examples:

+     *       containsWordIgnoreCase("ABc def", "abc") == true
+     *       containsWordIgnoreCase("ABc def", "DEF") == true
+     *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
+     *       
+ * @param sentence cannot be null + * @param word cannot be null, cannot be empty, must be a single word + */ + public static boolean containsWordIgnoreCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + + String preppedSentence = sentence; + String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + + for (String wordInSentence: wordsInPreppedSentence) { + if (wordInSentence.equalsIgnoreCase(preppedWord)) { + return true; + } + } + return false; } /** * Returns a detailed message of the t, including the stack trace. */ - public static String getDetails(Throwable t){ - assert t != null; + public static String getDetails(Throwable t) { + requireNonNull(t); StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); return t.getMessage() + "\n" + sw.toString(); } /** - * Returns true if s represents an unsigned integer e.g. 1, 2, 3, ...
- * Will return false for null, empty string, "-1", "0", "+1", and " 2 " (untrimmed) "3 0" (contains whitespace). - * @param s Should be trimmed. + * Returns true if {@code s} represents a non-zero unsigned integer + * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
+ * Will return false for any other non-null string input + * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * @throws NullPointerException if {@code s} is null. */ - public static boolean isUnsignedInteger(String s){ - return s != null && s.matches("^0*[1-9]\\d*$"); + public static boolean isNonZeroUnsignedInteger(String s) { + requireNonNull(s); + + try { + int value = Integer.parseInt(s); + return value > 0 && !s.startsWith("+"); // "+1" is successfully parsed by Integer#parseInt(String) + } catch (NumberFormatException nfe) { + return false; + } } } diff --git a/src/main/java/seedu/address/commons/util/UrlUtil.java b/src/main/java/seedu/address/commons/util/UrlUtil.java deleted file mode 100644 index 6bbab52b9840..000000000000 --- a/src/main/java/seedu/address/commons/util/UrlUtil.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.commons.util; - -import java.net.URL; - -/** - * A utility class for URL - */ -public class UrlUtil { - - /** - * Returns true if both URLs have the same base URL - */ - public static boolean compareBaseUrls(URL url1, URL url2) { - - if (url1 == null || url2 == null) { - return false; - } - return url1.getHost().toLowerCase().replaceFirst("www.", "") - .equals(url2.getHost().replaceFirst("www.", "").toLowerCase()) - && url1.getPath().replaceAll("/", "").toLowerCase() - .equals(url2.getPath().replaceAll("/", "").toLowerCase()); - } - -} diff --git a/src/main/java/seedu/address/commons/util/XmlUtil.java b/src/main/java/seedu/address/commons/util/XmlUtil.java index 2087e7628a1d..5f61738627cc 100644 --- a/src/main/java/seedu/address/commons/util/XmlUtil.java +++ b/src/main/java/seedu/address/commons/util/XmlUtil.java @@ -1,11 +1,14 @@ package seedu.address.commons.util; +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.FileNotFoundException; + import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import java.io.File; -import java.io.FileNotFoundException; /** * Helps with reading from and writing to XML files. @@ -26,8 +29,8 @@ public class XmlUtil { public static T getDataFromFile(File file, Class classToConvert) throws FileNotFoundException, JAXBException { - assert file != null; - assert classToConvert != null; + requireNonNull(file); + requireNonNull(classToConvert); if (!FileUtil.isFileExists(file)) { throw new FileNotFoundException("File not found : " + file.getAbsolutePath()); @@ -50,8 +53,8 @@ public static T getDataFromFile(File file, Class classToConvert) */ public static void saveDataToFile(File file, T data) throws FileNotFoundException, JAXBException { - assert file != null; - assert data != null; + requireNonNull(file); + requireNonNull(data); if (!file.exists()) { throw new FileNotFoundException("File not found : " + file.getAbsolutePath()); diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/seedu/address/logic/CommandHistory.java new file mode 100644 index 000000000000..10821acb3e2d --- /dev/null +++ b/src/main/java/seedu/address/logic/CommandHistory.java @@ -0,0 +1,32 @@ +package seedu.address.logic; + +import static java.util.Objects.requireNonNull; + +import java.util.LinkedList; +import java.util.List; + +/** + * Stores the history of commands executed. + */ +public class CommandHistory { + private LinkedList userInputHistory; + + public CommandHistory() { + userInputHistory = new LinkedList<>(); + } + + /** + * Appends {@code userInput} to the list of user input entered. + */ + public void add(String userInput) { + requireNonNull(userInput); + userInputHistory.add(userInput); + } + + /** + * Returns a defensive copy of {@code userInputHistory}. + */ + public List getHistory() { + return new LinkedList<>(userInputHistory); + } +} diff --git a/src/main/java/seedu/address/logic/ListElementPointer.java b/src/main/java/seedu/address/logic/ListElementPointer.java new file mode 100644 index 000000000000..21302ad1933a --- /dev/null +++ b/src/main/java/seedu/address/logic/ListElementPointer.java @@ -0,0 +1,110 @@ +package seedu.address.logic; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Has a cursor that points to an element in the list, and is able to iterate through the list. + * This is different from {@code ListIterator}, which has a cursor that points in between elements. + * The {@code ListIterator}'s behaviour: when making alternating calls of {@code next()} and + * {@code previous()}, the same element is returned on both calls. + * In contrast, {@code ListElementPointer}'s behaviour: when making alternating calls of + * {@code next()} and {@code previous()}, the next and previous elements are returned respectively. + */ +public class ListElementPointer { + private List list; + private int index; + + /** + * Constructs {@code ListElementPointer} which is backed by a defensive copy of {@code list}. + * The cursor points to the last element in {@code list}. + */ + public ListElementPointer(List list) { + this.list = list; + index = this.list.size() - 1; + } + + /** + * Appends {@code element} to the end of the list. + */ + public void add(String element) { + list.add(element); + } + + /** + * Returns true if calling {@code #next()} does not throw an {@code NoSuchElementException}. + */ + public boolean hasNext() { + int nextIndex = index + 1; + return isWithinBounds(nextIndex); + } + + /** + * Returns true if calling {@code #previous()} does not throw an {@code NoSuchElementException}. + */ + public boolean hasPrevious() { + int previousIndex = index - 1; + return isWithinBounds(previousIndex); + } + + /** + * Returns true if calling {@code #current()} does not throw an {@code NoSuchElementException}. + */ + public boolean hasCurrent() { + return isWithinBounds(index); + } + + private boolean isWithinBounds(int index) { + return index >= 0 && index < list.size(); + } + + /** + * Returns the next element in the list and advances the cursor position. + * @throws NoSuchElementException if there is no more next element in the list. + */ + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return list.get(++index); + } + + /** + * Returns the previous element in the list and moves the cursor position backwards. + * @throws NoSuchElementException if there is no more previous element in the list. + */ + public String previous() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + return list.get(--index); + } + + /** + * Returns the current element in the list. + * @throws NoSuchElementException if the list is empty. + */ + public String current() { + if (!hasCurrent()) { + throw new NoSuchElementException(); + } + return list.get(index); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ListElementPointer)) { + return false; + } + + // state check + ListElementPointer iterator = (ListElementPointer) other; + return list.equals(iterator.list) && index == iterator.index; + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 4df1bc65cabb..f6000c79262c 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -2,6 +2,8 @@ import javafx.collections.ObservableList; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.ReadOnlyPerson; /** @@ -12,10 +14,14 @@ public interface Logic { * Executes the command and returns the result. * @param commandText The command as entered by the user. * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText); + CommandResult execute(String commandText) throws CommandException, ParseException; - /** Returns the filtered list of persons */ + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ + ListElementPointer getHistorySnapshot(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index ce4dc1903cff..f60c7aeebda8 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -1,16 +1,17 @@ package seedu.address.logic; +import java.util.logging.Logger; + import javafx.collections.ObservableList; import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.parser.Parser; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.storage.Storage; - -import java.util.logging.Logger; /** * The main LogicManager of the app. @@ -19,23 +20,38 @@ public class LogicManager extends ComponentManager implements Logic { private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; - private final Parser parser; + private final CommandHistory history; + private final AddressBookParser addressBookParser; + private final UndoRedoStack undoRedoStack; - public LogicManager(Model model, Storage storage) { + public LogicManager(Model model) { this.model = model; - this.parser = new Parser(); + this.history = new CommandHistory(); + this.addressBookParser = new AddressBookParser(); + this.undoRedoStack = new UndoRedoStack(); } @Override - public CommandResult execute(String commandText) { + public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); - Command command = parser.parseCommand(commandText); - command.setData(model); - return command.execute(); + try { + Command command = addressBookParser.parseCommand(commandText); + command.setData(model, history, undoRedoStack); + CommandResult result = command.execute(); + undoRedoStack.push(command); + return result; + } finally { + history.add(commandText); + } } @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + + @Override + public ListElementPointer getHistorySnapshot() { + return new ListElementPointer(history.getHistory()); + } } diff --git a/src/main/java/seedu/address/logic/UndoRedoStack.java b/src/main/java/seedu/address/logic/UndoRedoStack.java new file mode 100644 index 000000000000..ddb62ef0ea87 --- /dev/null +++ b/src/main/java/seedu/address/logic/UndoRedoStack.java @@ -0,0 +1,89 @@ +package seedu.address.logic; + +import java.util.Stack; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.UndoableCommand; + +/** + * Maintains the undo-stack (the stack of commands that can be undone) and the redo-stack (the stack of + * commands that can be undone). + */ +public class UndoRedoStack { + private Stack undoStack; + private Stack redoStack; + + public UndoRedoStack() { + undoStack = new Stack<>(); + redoStack = new Stack<>(); + } + + /** + * Pushes {@code command} onto the undo-stack if it is of type {@code UndoableCommand}. Clears the redo-stack + * if {@code command} is not of type {@code UndoCommand} or {@code RedoCommand}. + */ + public void push(Command command) { + if (!(command instanceof UndoCommand) && !(command instanceof RedoCommand)) { + redoStack.clear(); + } + + if (!(command instanceof UndoableCommand)) { + return; + } + + undoStack.add((UndoableCommand) command); + } + + /** + * Pops and returns the next {@code UndoableCommand} to be undone in the stack. + */ + public UndoableCommand popUndo() { + UndoableCommand toUndo = undoStack.pop(); + redoStack.push(toUndo); + return toUndo; + } + + /** + * Pops and returns the next {@code UndoableCommand} to be redone in the stack. + */ + public UndoableCommand popRedo() { + UndoableCommand toRedo = redoStack.pop(); + undoStack.push(toRedo); + return toRedo; + } + + /** + * Returns true if there are more commands that can be undone. + */ + public boolean canUndo() { + return !undoStack.empty(); + } + + /** + * Returns true if there are more commands that can be redone. + */ + public boolean canRedo() { + return !redoStack.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UndoRedoStack)) { + return false; + } + + UndoRedoStack stack = (UndoRedoStack) other; + + // state check + return undoStack.equals(stack.undoStack) + && redoStack.equals(stack.redoStack); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 2860a9ab2a85..63ee5489d443 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,24 +1,47 @@ package seedu.address.logic.commands; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.HashSet; -import java.util.Set; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.person.exceptions.DuplicatePersonException; /** * Adds a person to the address book. */ -public class AddCommand extends Command { +public class AddCommand extends UndoableCommand { + public static final String[] COMMAND_WORDS = {"add", "a", "+"}; public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: NAME p/PHONE e/EMAIL a/ADDRESS [t/TAG]...\n" - + "Example: " + COMMAND_WORD - + " John Doe p/98765432 e/johnd@gmail.com a/311, Clementi Ave 2, #02-25 t/friends t/owesMoney"; + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + + ": Adds a person to the address book. " + + "Parameters: " + + PREFIX_NAME + "NAME " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_DOB + "DATE OF BIRTH] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "John Doe " + + PREFIX_PHONE + "98765432 " + + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_DOB + "20 01 1997 " + + PREFIX_TAG + "friends " + + PREFIX_TAG + "owesMoney"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; @@ -26,35 +49,100 @@ public class AddCommand extends Command { private final Person toAdd; /** - * Convenience constructor using raw values. - * - * @throws IllegalValueException if any of the raw values are invalid + * Creates an AddCommand to add the specified {@code ReadOnlyPerson} */ - public AddCommand(String name, String phone, String email, String address, Set tags) - throws IllegalValueException { - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(new Tag(tagName)); - } - this.toAdd = new Person( - new Name(name), - new Phone(phone), - new Email(email), - new Address(address), - new UniqueTagList(tagSet) - ); + public AddCommand(ReadOnlyPerson person) { + toAdd = new Person(person); } @Override - public CommandResult execute() { - assert model != null; + public CommandResult executeUndoableCommand() throws CommandException { + requireNonNull(model); try { model.addPerson(toAdd); return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } catch (UniquePersonList.DuplicatePersonException e) { - return new CommandResult(MESSAGE_DUPLICATE_PERSON); + } catch (DuplicatePersonException e) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); } } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddCommand // instanceof handles nulls + && toAdd.equals(((AddCommand) other).toAdd)); + } + + /** + * Stores the optional details to add the person with. By default each field is an object + * with value of empty String. + */ + public static class AddPersonOptionalFieldDescriptor { + private Phone phone; + private Email email; + private Address address; + private DateOfBirth dateofbirth; + + public AddPersonOptionalFieldDescriptor() { + this.phone = new Phone(); + this.email = new Email(); + this.address = new Address(); + this.dateofbirth = new DateOfBirth(); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Phone getPhone() { + return phone; + } + + public void setEmail(Email email) { + this.email = email; + } + + public Email getEmail() { + return email; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Address getAddress() { + return address; + } + + public void setDateOfBirth(DateOfBirth dateofbirth) { + this.dateofbirth = dateofbirth; + } + + public DateOfBirth getDateOfBirth() { + return dateofbirth; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddCommand.AddPersonOptionalFieldDescriptor)) { + return false; + } + + // state check + AddCommand.AddPersonOptionalFieldDescriptor a = + (AddCommand.AddPersonOptionalFieldDescriptor) other; + + return getPhone().equals(a.getPhone()) + && getEmail().equals(a.getEmail()) + && getAddress().equals(a.getAddress()) + && getDateOfBirth().equals(a.getDateOfBirth()); + } + } } diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 522d57189f51..d78b9b10b4ab 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -1,22 +1,23 @@ package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; + import seedu.address.model.AddressBook; /** * Clears the address book. */ -public class ClearCommand extends Command { +public class ClearCommand extends UndoableCommand { + public static final String[] COMMAND_WORDS = {"clear", "clr", "c", "cl"}; public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - public ClearCommand() {} - @Override - public CommandResult execute() { - assert model != null; - model.resetData(AddressBook.getEmptyAddressBook()); + public CommandResult executeUndoableCommand() { + requireNonNull(model); + model.resetData(new AddressBook()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 7c0ba2fd0161..209d0032be2d 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -1,8 +1,9 @@ package seedu.address.logic.commands; -import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.Messages; -import seedu.address.commons.events.ui.IncorrectCommandAttemptedEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; /** @@ -10,6 +11,8 @@ */ public abstract class Command { protected Model model; + protected CommandHistory history; + protected UndoRedoStack undoRedoStack; /** * Constructs a feedback message to summarise an operation that displayed a listing of persons. @@ -25,22 +28,30 @@ public static String getMessageForPersonListShownSummary(int displaySize) { * Executes the command and returns the result message. * * @return feedback message of the operation result for display + * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(); + public abstract CommandResult execute() throws CommandException; /** * Provides any needed dependencies to the command. * Commands making use of any of these should override this method to gain * access to the dependencies. */ - public void setData(Model model) { + public void setData(Model model, CommandHistory history, UndoRedoStack undoRedoStack) { this.model = model; } /** - * Raises an event to indicate an attempt to execute an incorrect command + * @return concatenated form of the array of command words to allow easier printing for the help command */ - protected void indicateAttemptToExecuteIncorrectCommand() { - EventsCenter.getInstance().post(new IncorrectCommandAttemptedEvent(this)); + public static String concatenateCommandWords(String[] commandWords) { + String finalWord = new String(); + for (int i = 0; i < commandWords.length - 1; i++) { + finalWord += commandWords[i] + " -OR- "; + } + finalWord += commandWords[commandWords.length - 1]; + return finalWord; } + + } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index f46f2f31353e..abdc267a2c44 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -1,5 +1,7 @@ package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; + /** * Represents the result of a command execution. */ @@ -8,8 +10,7 @@ public class CommandResult { public final String feedbackToUser; public CommandResult(String feedbackToUser) { - assert feedbackToUser != null; - this.feedbackToUser = feedbackToUser; + this.feedbackToUser = requireNonNull(feedbackToUser); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1bfebe8912a8..f011246f7ad8 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,42 +1,45 @@ package seedu.address.logic.commands; +import java.util.List; + import seedu.address.commons.core.Messages; -import seedu.address.commons.core.UnmodifiableObservableList; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList.PersonNotFoundException; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * Deletes a person identified using it's last displayed index from the address book. */ -public class DeleteCommand extends Command { +public class DeleteCommand extends UndoableCommand { + public static final String[] COMMAND_WORDS = {"delete", "del", "d", "-"}; public static final String COMMAND_WORD = "delete"; - public static final String MESSAGE_USAGE = COMMAND_WORD + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + ": Deletes the person identified by the index number used in the last person listing.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - public final int targetIndex; + private final Index targetIndex; - public DeleteCommand(int targetIndex) { + public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } @Override - public CommandResult execute() { + public CommandResult executeUndoableCommand() throws CommandException { - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getFilteredPersonList(); - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - ReadOnlyPerson personToDelete = lastShownList.get(targetIndex - 1); + ReadOnlyPerson personToDelete = lastShownList.get(targetIndex.getZeroBased()); try { model.deletePerson(personToDelete); @@ -47,4 +50,10 @@ public CommandResult execute() { return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteCommand // instanceof handles nulls + && this.targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check + } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java new file mode 100644 index 000000000000..65c3b345da2f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -0,0 +1,258 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELTAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing person in the address book. + */ +public class EditCommand extends UndoableCommand { + + public static final String[] COMMAND_WORDS = {"edit", "e", "change", "ed"}; + public static final String COMMAND_WORD = "edit"; + + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + + ": Edits the details of the person identified " + + "by the index number used in the last person listing. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_DOB + "DATE OF BIRTH] " + + "[" + PREFIX_TAG + "TAG] " + + "[" + PREFIX_DELTAG + "TAG_TO_DELETE] ...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + private final Index index; + private final EditPersonDescriptor editPersonDescriptor; + + /** + * @param index of the person in the filtered person list to edit + * @param editPersonDescriptor details to edit the person with + */ + public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + requireNonNull(index); + requireNonNull(editPersonDescriptor); + + this.index = index; + this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + ReadOnlyPerson personToEdit = lastShownList.get(index.getZeroBased()); + Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + + try { + model.updatePerson(personToEdit, editedPerson); + } catch (DuplicatePersonException dpe) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * edited with {@code editPersonDescriptor}. + */ + private static Person createEditedPerson(ReadOnlyPerson personToEdit, + EditPersonDescriptor editPersonDescriptor) { + assert personToEdit != null; + + Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + DateOfBirth updatedDateOfBirth = editPersonDescriptor.getDateOfBirth().orElse(personToEdit.getDateOfBirth()); + Set updatedTags = personToEdit.getTags(); + + if (editPersonDescriptor.getTagsToDel().isPresent()) { + for (Tag tag : editPersonDescriptor.getTagsToDel().get()) { + if (tag.getTagName().equals("all")) { + updatedTags.clear(); + } + } + updatedTags.removeAll(editPersonDescriptor.getTagsToDel().get()); + } + + if (editPersonDescriptor.getTags().isPresent()) { + updatedTags.addAll(editPersonDescriptor.getTags().get()); + } + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedDateOfBirth, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditCommand)) { + return false; + } + + // state check + EditCommand e = (EditCommand) other; + return index.equals(e.index) + && editPersonDescriptor.equals(e.editPersonDescriptor); + } + + /** + * Stores the details to edit the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class EditPersonDescriptor { + private Name name; + private Phone phone; + private Email email; + private Address address; + private DateOfBirth dob; + private Set tags; + private Set tagsToDel; + + public EditPersonDescriptor() {} + + public EditPersonDescriptor(EditPersonDescriptor toCopy) { + this.name = toCopy.name; + this.phone = toCopy.phone; + this.email = toCopy.email; + this.address = toCopy.address; + this.dob = toCopy.dob; + this.tags = toCopy.tags; + this.tagsToDel = toCopy.tagsToDel; + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull( + this.name, this.phone, this.email, this.address, this.dob, this.tags, this.tagsToDel); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + public void setDateOfBirth(DateOfBirth dob) { + this.dob = dob; + } + + public Optional getDateOfBirth() { + return Optional.ofNullable(dob); + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Optional> getTags() { + return Optional.ofNullable(tags); + } + + public void setTagsToDel(Set tagsToDel) { + this.tagsToDel = tagsToDel; + } + + public Optional> getTagsToDel() { + return Optional.ofNullable(tagsToDel); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPersonDescriptor)) { + return false; + } + + // state check + EditPersonDescriptor e = (EditPersonDescriptor) other; + + return getName().equals(e.getName()) + && getPhone().equals(e.getPhone()) + && getEmail().equals(e.getEmail()) + && getAddress().equals(e.getAddress()) + && getDateOfBirth().equals(e.getDateOfBirth()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index d98233ce2a0b..bae425aef902 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -8,12 +8,11 @@ */ public class ExitCommand extends Command { + public static final String[] COMMAND_WORDS = {"exit", "quit", "esc", "off"}; public static final String COMMAND_WORD = "exit"; public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; - public ExitCommand() {} - @Override public CommandResult execute() { EventsCenter.getInstance().post(new ExitAppRequestEvent()); diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 1d61bf6cc857..cef2381b2190 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,6 +1,6 @@ package seedu.address.logic.commands; -import java.util.Set; +import seedu.address.model.person.NameContainsKeywordsPredicate; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. @@ -8,23 +8,31 @@ */ public class FindCommand extends Command { + public static final String[] COMMAND_WORDS = {"find", "f", "look", "lookup"}; public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + + ": Finds all persons whose names contain any of " + "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; - private final Set keywords; + private final NameContainsKeywordsPredicate predicate; - public FindCommand(Set keywords) { - this.keywords = keywords; + public FindCommand(NameContainsKeywordsPredicate predicate) { + this.predicate = predicate; } @Override public CommandResult execute() { - model.updateFilteredPersonList(keywords); + model.updateFilteredPersonList(predicate); return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size())); } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindCommand // instanceof handles nulls + && this.predicate.equals(((FindCommand) other).predicate)); // state check + } } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index 65af96940242..43a86000e672 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -1,6 +1,5 @@ package seedu.address.logic.commands; - import seedu.address.commons.core.EventsCenter; import seedu.address.commons.events.ui.ShowHelpRequestEvent; @@ -9,15 +8,15 @@ */ public class HelpCommand extends Command { + public static final String[] COMMAND_WORDS = {"help", "h", "hlp", "f1", "commands", "command", "sos"}; public static final String COMMAND_WORD = "help"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; public static final String SHOWING_HELP_MESSAGE = "Opened help window."; - public HelpCommand() {} - @Override public CommandResult execute() { EventsCenter.getInstance().post(new ShowHelpRequestEvent()); diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java new file mode 100644 index 000000000000..4345ab13eeb6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/HistoryCommand.java @@ -0,0 +1,39 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; + +/** + * Lists all the commands entered by user from the start of app launch. + */ +public class HistoryCommand extends Command { + + public static final String[] COMMAND_WORDS = {"history", "last", "h", "hist"}; + public static final String COMMAND_WORD = "history"; + public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; + public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; + + @Override + public CommandResult execute() { + List previousCommands = history.getHistory(); + + if (previousCommands.isEmpty()) { + return new CommandResult(MESSAGE_NO_HISTORY); + } + + Collections.reverse(previousCommands); + return new CommandResult(String.format(MESSAGE_SUCCESS, String.join("\n", previousCommands))); + } + + @Override + public void setData(Model model, CommandHistory history, UndoRedoStack undoRedoStack) { + requireNonNull(history); + this.history = history; + } +} diff --git a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java b/src/main/java/seedu/address/logic/commands/IncorrectCommand.java deleted file mode 100644 index 491d9cb9da35..000000000000 --- a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java +++ /dev/null @@ -1,22 +0,0 @@ -package seedu.address.logic.commands; - - -/** - * Represents an incorrect command. Upon execution, produces some feedback to the user. - */ -public class IncorrectCommand extends Command { - - public final String feedbackToUser; - - public IncorrectCommand(String feedbackToUser){ - this.feedbackToUser = feedbackToUser; - } - - @Override - public CommandResult execute() { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(feedbackToUser); - } - -} - diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 9bdd457a1b01..0696e7ee7fe8 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,20 +1,21 @@ package seedu.address.logic.commands; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; /** * Lists all persons in the address book to the user. */ public class ListCommand extends Command { + public static final String[] COMMAND_WORDS = {"list"}; public static final String COMMAND_WORD = "list"; public static final String MESSAGE_SUCCESS = "Listed all persons"; - public ListCommand() {} @Override public CommandResult execute() { - model.updateFilteredListToShowAll(); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/PartialFindCommand.java b/src/main/java/seedu/address/logic/commands/PartialFindCommand.java new file mode 100644 index 000000000000..0cdc6f0bae39 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PartialFindCommand.java @@ -0,0 +1,38 @@ +package seedu.address.logic.commands; + +import seedu.address.model.person.NameStartsWithKeywordsPredicate; + +/** + * Finds and lists all persons in address book whose name starts with any of the argument keywords. + * Keyword matching is case sensitive. + */ +public class PartialFindCommand extends Command { + + public static final String[] COMMAND_WORDS = {"pfind", "pf", "plook", "plookup"}; + public static final String COMMAND_WORD = "pfind"; + + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + + ": Finds all persons whose names starts with " + + "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " Ali Bo Ch"; + + private final NameStartsWithKeywordsPredicate predicate; + + public PartialFindCommand (NameStartsWithKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute() { + model.updateFilteredPersonList(predicate); + return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PartialFindCommand // instanceof handles nulls + && this.predicate.equals(((PartialFindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java new file mode 100644 index 000000000000..2469ea86c922 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Redo the previously undone command. + */ +public class RedoCommand extends Command { + + public static final String[] COMMAND_WORDS = {"redo", "r"}; + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Redo success!"; + public static final String MESSAGE_FAILURE = "No more commands to redo!"; + + @Override + public CommandResult execute() throws CommandException { + requireAllNonNull(model, undoRedoStack); + + if (!undoRedoStack.canRedo()) { + throw new CommandException(MESSAGE_FAILURE); + } + + undoRedoStack.popRedo().redo(); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public void setData(Model model, CommandHistory commandHistory, UndoRedoStack undoRedoStack) { + this.model = model; + this.undoRedoStack = undoRedoStack; + } +} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java index 9ca0551f1951..659dc22a4a3e 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java @@ -1,9 +1,12 @@ package seedu.address.logic.commands; +import java.util.List; + import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.core.UnmodifiableObservableList; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.person.ReadOnlyPerson; /** @@ -11,34 +14,40 @@ */ public class SelectCommand extends Command { - public final int targetIndex; - + public static final String[] COMMAND_WORDS = {"select", "s", "choose", "sel"}; public static final String COMMAND_WORD = "select"; - public static final String MESSAGE_USAGE = COMMAND_WORD + public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS) + ": Selects the person identified by the index number used in the last person listing.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; - public SelectCommand(int targetIndex) { + private final Index targetIndex; + + public SelectCommand(Index targetIndex) { this.targetIndex = targetIndex; } @Override - public CommandResult execute() { + public CommandResult execute() throws CommandException { - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getFilteredPersonList(); - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex - 1)); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex)); + EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex)); + return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SelectCommand // instanceof handles nulls + && this.targetIndex.equals(((SelectCommand) other).targetIndex)); // state check + } } diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java new file mode 100644 index 000000000000..90d0981c869c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Undo the previous {@code UndoableCommand}. + */ +public class UndoCommand extends Command { + + public static final String[] COMMAND_WORDS = {"undo", "u", "revert"}; + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Undo success!"; + public static final String MESSAGE_FAILURE = "No more commands to undo!"; + + @Override + public CommandResult execute() throws CommandException { + requireAllNonNull(model, undoRedoStack); + + if (!undoRedoStack.canUndo()) { + throw new CommandException(MESSAGE_FAILURE); + } + + undoRedoStack.popUndo().undo(); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public void setData(Model model, CommandHistory commandHistory, UndoRedoStack undoRedoStack) { + this.model = model; + this.undoRedoStack = undoRedoStack; + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoableCommand.java b/src/main/java/seedu/address/logic/commands/UndoableCommand.java new file mode 100644 index 000000000000..1ba888ead594 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoableCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; + +/** + * Represents a command which can be undone and redone. + */ +public abstract class UndoableCommand extends Command { + private ReadOnlyAddressBook previousAddressBook; + + protected abstract CommandResult executeUndoableCommand() throws CommandException; + + /** + * Stores the current state of {@code model#addressBook}. + */ + private void saveAddressBookSnapshot() { + requireNonNull(model); + this.previousAddressBook = new AddressBook(model.getAddressBook()); + } + + /** + * Reverts the AddressBook to the state before this command + * was executed and updates the filtered person list to + * show all persons. + */ + protected final void undo() { + requireAllNonNull(model, previousAddressBook); + model.resetData(previousAddressBook); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + + /** + * Executes the command and updates the filtered person + * list to show all persons. + */ + protected final void redo() { + requireNonNull(model); + try { + executeUndoableCommand(); + } catch (CommandException ce) { + throw new AssertionError("The command has been successfully executed previously; " + + "it should not fail now"); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + + @Override + public final CommandResult execute() throws CommandException { + saveAddressBookSnapshot(); + return executeUndoableCommand(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/WhyCommand.java b/src/main/java/seedu/address/logic/commands/WhyCommand.java new file mode 100644 index 000000000000..e4671bc3d009 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/WhyCommand.java @@ -0,0 +1,54 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.model.person.Address; +import seedu.address.model.person.Name; +import seedu.address.model.person.ReadOnlyPerson; + + +/** + * Format full help instructions for every command for display. + */ +public class WhyCommand extends Command { + + public static final String[] COMMAND_WORDS = {"why"}; + public static final String COMMAND_WORD = "why"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Tells you why.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_WHY_REMARK_SUCCESS = "Added remark to Person: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + public static final String SHOWING_WHY_MESSAGE = "Because %1$s lives in \n%2$s"; + + private final Index targetIndex; + + public WhyCommand(Index targetIndex) { + requireNonNull(targetIndex); + + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute() { + //EventsCenter.getInstance().post(new ShowHelpRequestEvent()); + + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + //throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + ReadOnlyPerson personToDelete = lastShownList.get(targetIndex.getZeroBased()); + Name name = personToDelete.getName(); + Address address = personToDelete.getAddress(); + String reason = personToDelete.getReason(); + //return new CommandResult(String.format(SHOWING_WHY_MESSAGE, name, address)); + return new CommandResult(reason); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java new file mode 100644 index 000000000000..ed23ad42eb26 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -0,0 +1,10 @@ +package seedu.address.logic.commands.exceptions; + +/** + * Represents an error which occurs during execution of a {@link Command}. + */ +public class CommandException extends Exception { + public CommandException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java new file mode 100644 index 000000000000..f8ab976109bb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -0,0 +1,82 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddCommand.AddPersonOptionalFieldDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap; + argMultimap = ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_DOB, PREFIX_TAG); + + if (!isNamePrefixPresent(argMultimap, PREFIX_NAME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + AddPersonOptionalFieldDescriptor addPersonOptionalFieldDescriptor = + new AddPersonOptionalFieldDescriptor(); + + try { + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME)).get(); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE)) + .ifPresent(addPersonOptionalFieldDescriptor::setPhone); + ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL)) + .ifPresent(addPersonOptionalFieldDescriptor::setEmail); + ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS)) + .ifPresent(addPersonOptionalFieldDescriptor::setAddress); + ParserUtil.parseDateOfBirth(argMultimap.getValue(PREFIX_DOB)) + .ifPresent(addPersonOptionalFieldDescriptor::setDateOfBirth); + + Phone phone = addPersonOptionalFieldDescriptor.getPhone(); + Email email = addPersonOptionalFieldDescriptor.getEmail(); + Address address = addPersonOptionalFieldDescriptor.getAddress(); + DateOfBirth dob = addPersonOptionalFieldDescriptor.getDateOfBirth(); + + ReadOnlyPerson person = new Person(name, phone, email, address, dob, tagList); + + return new AddCommand(person); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + } + + /** + * Returns true if the name prefixes does not contain empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean isNamePrefixPresent(ArgumentMultimap argumentMultimap, Prefix namePrefix) { + return argumentMultimap.getValue(namePrefix).isPresent(); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java new file mode 100644 index 000000000000..aaa5f32c08bf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -0,0 +1,191 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.PartialFindCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.WhyCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class AddressBookParser { + + /** + * Enumerator list to define the types of commands. + */ + private enum CommandType { + ADD, CLEAR, DEL, EDIT, EXIT, FIND, PFIND, HELP, HISTORY, LIST, REDO, UNDO, SELECT, WHY, NONE + } + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + CommandType commandType = getCommandType(commandWord.toLowerCase()); + + switch (commandType) { + + case ADD: + return new AddCommandParser().parse(arguments); + + case EDIT: + return new EditCommandParser().parse(arguments); + + case SELECT: + return new SelectCommandParser().parse(arguments); + + case DEL: + return new DeleteCommandParser().parse(arguments); + + case CLEAR: + return new ClearCommand(); + + case FIND: + return new FindCommandParser().parse(arguments); + + case PFIND: + return new PartialFindCommandParser().parse(arguments); + + case LIST: + return new ListCommand(); + + case HISTORY: + return new HistoryCommand(); + + case EXIT: + return new ExitCommand(); + + case HELP: + return new HelpCommand(); + + case UNDO: + return new UndoCommand(); + + case REDO: + return new RedoCommand(); + + case WHY: + return new WhyCommandParser().parse(arguments); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * Searches the entire list of acceptable command words in each command and returns the enumerated value type. + * @param commandWord + * @return enumerated value for the switch statement to process + */ + + private CommandType getCommandType(String commandWord) { + for (String word : AddCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.ADD; + } + } + for (String word : ClearCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.CLEAR; + } + } + for (String word : DeleteCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.DEL; + } + } + for (String word : EditCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.EDIT; + } + } + for (String word : ExitCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.EXIT; + } + } + for (String word : FindCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.FIND; + } + } + for (String word : PartialFindCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.PFIND; + } + } + for (String word : HelpCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.HELP; + } + } + for (String word : HistoryCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.HISTORY; + } + } + for (String word : ListCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.LIST; + } + } + for (String word : RedoCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.REDO; + } + } + for (String word : SelectCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.SELECT; + } + } + for (String word : UndoCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.UNDO; + } + } + for (String word : WhyCommand.COMMAND_WORDS) { + if (commandWord.contentEquals(word)) { + return CommandType.WHY; + } + } + return CommandType.NONE; + } + + +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java new file mode 100644 index 000000000000..954c8e18f8ea --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -0,0 +1,60 @@ +package seedu.address.logic.parser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Stores mapping of prefixes to their respective arguments. + * Each key may be associated with multiple argument values. + * Values for a given key are stored in a list, and the insertion ordering is maintained. + * Keys are unique, but the list of argument values may contain duplicate argument values, i.e. the same argument value + * can be inserted multiple times for the same prefix. + */ +public class ArgumentMultimap { + + /** Prefixes mapped to their respective arguments**/ + private final Map> argMultimap = new HashMap<>(); + + /** + * Associates the specified argument value with {@code prefix} key in this map. + * If the map previously contained a mapping for the key, the new value is appended to the list of existing values. + * + * @param prefix Prefix key with which the specified argument value is to be associated + * @param argValue Argument value to be associated with the specified prefix key + */ + public void put(Prefix prefix, String argValue) { + List argValues = getAllValues(prefix); + argValues.add(argValue); + argMultimap.put(prefix, argValues); + } + + /** + * Returns the last value of {@code prefix}. + */ + public Optional getValue(Prefix prefix) { + List values = getAllValues(prefix); + return values.isEmpty() ? Optional.empty() : Optional.of(values.get(values.size() - 1)); + } + + /** + * Returns all values of {@code prefix}. + * If the prefix does not exist or has no values, this will return an empty list. + * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. + */ + public List getAllValues(Prefix prefix) { + if (!argMultimap.containsKey(prefix)) { + return new ArrayList<>(); + } + return new ArrayList<>(argMultimap.get(prefix)); + } + + /** + * Returns the preamble (text before the first valid prefix). Trims any leading/trailing spaces. + */ + public String getPreamble() { + return getValue(new Prefix("")).orElse(""); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java new file mode 100644 index 000000000000..a1bddbb6b979 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -0,0 +1,150 @@ +package seedu.address.logic.parser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tokenizes arguments string of the form: {@code preamble value value ...}
+ * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
+ * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
+ * 2. Leading and trailing whitespaces of an argument value will be discarded.
+ * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} + * in the above example.
+ */ +public class ArgumentTokenizer { + + /** + * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps prefixes to their + * respective argument values. Only the given prefixes will be recognized in the arguments string. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param prefixes Prefixes to tokenize the arguments string with + * @return ArgumentMultimap object that maps prefixes to their arguments + */ + public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { + List positions = findAllPrefixPositions(argsString, prefixes); + return extractArguments(argsString, positions); + } + + /** + * Finds all zero-based prefix positions in the given arguments string. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param prefixes Prefixes to find in the arguments string + * @return List of zero-based prefix positions in the given arguments string + */ + private static List findAllPrefixPositions(String argsString, Prefix... prefixes) { + List positions = new ArrayList<>(); + + for (Prefix prefix : prefixes) { + positions.addAll(findPrefixPositions(argsString, prefix)); + } + + return positions; + } + + /** + * {@see findAllPrefixPositions} + */ + private static List findPrefixPositions(String argsString, Prefix prefix) { + List positions = new ArrayList<>(); + + int prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), 0); + while (prefixPosition != -1) { + PrefixPosition extendedPrefix = new PrefixPosition(prefix, prefixPosition); + positions.add(extendedPrefix); + prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition); + } + + return positions; + } + + /** + * Returns the index of the first occurrence of {@code prefix} in + * {@code argsString} starting from index {@code fromIndex}. An occurrence + * is valid if there is a whitespace before {@code prefix}. Returns -1 if no + * such occurrence can be found. + * + * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and + * {@code fromIndex} = 0, this method returns -1 as there are no valid + * occurrences of "p/" with whitespace before it. However, if + * {@code argsString} = "e/hi p/900", {@code prefix} = "p/" and + * {@code fromIndex} = 0, this method returns 5. + */ + private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { + int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); + return prefixIndex == -1 ? -1 + : prefixIndex + 1; // +1 as offset for whitespace + } + + /** + * Extracts prefixes and their argument values, and returns an {@code ArgumentMultimap} object that maps the + * extracted prefixes to their respective arguments. Prefixes are extracted based on their zero-based positions in + * {@code argsString}. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} + * @return ArgumentMultimap object that maps prefixes to their arguments + */ + private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) { + + // Sort by start position + prefixPositions.sort((prefix1, prefix2) -> prefix1.getStartPosition() - prefix2.getStartPosition()); + + // Insert a PrefixPosition to represent the preamble + PrefixPosition preambleMarker = new PrefixPosition(new Prefix(""), 0); + prefixPositions.add(0, preambleMarker); + + // Add a dummy PrefixPosition to represent the end of the string + PrefixPosition endPositionMarker = new PrefixPosition(new Prefix(""), argsString.length()); + prefixPositions.add(endPositionMarker); + + // Map prefixes to their argument values (if any) + ArgumentMultimap argMultimap = new ArgumentMultimap(); + for (int i = 0; i < prefixPositions.size() - 1; i++) { + // Extract and store prefixes and their arguments + Prefix argPrefix = prefixPositions.get(i).getPrefix(); + String argValue = extractArgumentValue(argsString, prefixPositions.get(i), prefixPositions.get(i + 1)); + argMultimap.put(argPrefix, argValue); + } + + return argMultimap; + } + + /** + * Returns the trimmed value of the argument in the arguments string specified by {@code currentPrefixPosition}. + * The end position of the value is determined by {@code nextPrefixPosition}. + */ + private static String extractArgumentValue(String argsString, + PrefixPosition currentPrefixPosition, + PrefixPosition nextPrefixPosition) { + Prefix prefix = currentPrefixPosition.getPrefix(); + + int valueStartPos = currentPrefixPosition.getStartPosition() + prefix.getPrefix().length(); + String value = argsString.substring(valueStartPos, nextPrefixPosition.getStartPosition()); + + return value.trim(); + } + + /** + * Represents a prefix's position in an arguments string. + */ + private static class PrefixPosition { + private int startPosition; + private final Prefix prefix; + + PrefixPosition(Prefix prefix, int startPosition) { + this.prefix = prefix; + this.startPosition = startPosition; + } + + int getStartPosition() { + return this.startPosition; + } + + Prefix getPrefix() { + return this.prefix; + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java new file mode 100644 index 000000000000..d9a1c1cc4ff2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -0,0 +1,16 @@ +package seedu.address.logic.parser; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions */ + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_PHONE = new Prefix("p/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_DELTAG = new Prefix("dt/"); + public static final Prefix PREFIX_DOB = new Prefix("d/"); +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java new file mode 100644 index 000000000000..fe9c1653850e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns an DeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteCommand(index); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java new file mode 100644 index 000000000000..86f171eb427e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -0,0 +1,86 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DELTAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_DOB, PREFIX_TAG, PREFIX_DELTAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + try { + ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME)).ifPresent(editPersonDescriptor::setName); + ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE)).ifPresent(editPersonDescriptor::setPhone); + ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL)).ifPresent(editPersonDescriptor::setEmail); + ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS)).ifPresent(editPersonDescriptor::setAddress); + ParserUtil.parseDateOfBirth(argMultimap.getValue(PREFIX_DOB)) + .ifPresent(editPersonDescriptor::setDateOfBirth); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_DELTAG)).ifPresent(editPersonDescriptor::setTagsToDel); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + + if (!editPersonDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + + return new EditCommand(index, editPersonDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws IllegalValueException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java new file mode 100644 index 000000000000..b186a967cb94 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.NameContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class FindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns an FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index 959b2cd0383c..d6551ad8e3ff 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -1,192 +1,16 @@ package seedu.address.logic.parser; -import seedu.address.logic.commands.*; -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.exceptions.IllegalValueException; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import seedu.address.logic.commands.Command; +import seedu.address.logic.parser.exceptions.ParseException; /** - * Parses user input. + * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. */ -public class Parser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - private static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); - - private static final Pattern KEYWORDS_ARGS_FORMAT = - Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace - - private static final Pattern PERSON_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes - Pattern.compile("(?[^/]+)" - + " (?p?)p/(?[^/]+)" - + " (?p?)e/(?[^/]+)" - + " (?p?)a/(?
[^/]+)" - + "(?(?: t/[^/]+)*)"); // variable number of tags - - public Parser() {} - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - */ - public Command parseCommand(String userInput) { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return prepareAdd(arguments); - - case SelectCommand.COMMAND_WORD: - return prepareSelect(arguments); - - case DeleteCommand.COMMAND_WORD: - return prepareDelete(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return prepareFind(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - return new IncorrectCommand(MESSAGE_UNKNOWN_COMMAND); - } - } +public interface Parser { /** - * Parses arguments in the context of the add person command. - * - * @param args full command args string - * @return the prepared command + * Parses {@code userInput} into a command and returns it. + * @throws ParseException if {@code userInput} does not conform the expected format */ - private Command prepareAdd(String args){ - final Matcher matcher = PERSON_DATA_ARGS_FORMAT.matcher(args.trim()); - // Validate arg string format - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - try { - return new AddCommand( - matcher.group("name"), - matcher.group("phone"), - matcher.group("email"), - matcher.group("address"), - getTagsFromArgs(matcher.group("tagArguments")) - ); - } catch (IllegalValueException ive) { - return new IncorrectCommand(ive.getMessage()); - } - } - - /** - * Extracts the new person's tags from the add command's tag arguments string. - * Merges duplicate tag strings. - */ - private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException { - // no tags - if (tagArguments.isEmpty()) { - return Collections.emptySet(); - } - // replace first delimiter prefix, then split - final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/")); - return new HashSet<>(tagStrings); - } - - /** - * Parses arguments in the context of the delete person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareDelete(String args) { - - Optional index = parseIndex(args); - if(!index.isPresent()){ - return new IncorrectCommand( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } - - return new DeleteCommand(index.get()); - } - - /** - * Parses arguments in the context of the select person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareSelect(String args) { - Optional index = parseIndex(args); - if(!index.isPresent()){ - return new IncorrectCommand( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE)); - } - - return new SelectCommand(index.get()); - } - - /** - * Returns the specified index in the {@code command} IF a positive unsigned integer is given as the index. - * Returns an {@code Optional.empty()} otherwise. - */ - private Optional parseIndex(String command) { - final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(command.trim()); - if (!matcher.matches()) { - return Optional.empty(); - } - - String index = matcher.group("targetIndex"); - if(!StringUtil.isUnsignedInteger(index)){ - return Optional.empty(); - } - return Optional.of(Integer.parseInt(index)); - - } - - /** - * Parses arguments in the context of the find person command. - * - * @param args full command args string - * @return the prepared command - */ - private Command prepareFind(String args) { - final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); - if (!matcher.matches()) { - return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, - FindCommand.MESSAGE_USAGE)); - } - - // keywords delimited by whitespace - final String[] keywords = matcher.group("keywords").split("\\s+"); - final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); - return new FindCommand(keywordSet); - } - -} \ No newline at end of file + T parse(String userInput) throws ParseException; +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java new file mode 100644 index 000000000000..65108274555c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -0,0 +1,100 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.StringUtil; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + * {@code ParserUtil} contains methods that take in {@code Optional} as parameters. However, it goes against Java's + * convention (see https://stackoverflow.com/a/39005452) as {@code Optional} should only be used a return type. + * Justification: The methods in concern receive {@code Optional} return values from other methods as parameters and + * return {@code Optional} values based on whether the parameters were present. Therefore, it is redundant to unwrap the + * initial {@code Optional} before passing to {@code ParserUtil} as a parameter and then re-wrap it into an + * {@code Optional} return value inside {@code ParserUtil} methods. + */ +public class ParserUtil { + + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INSUFFICIENT_PARTS = "Number of parts must be more than 1."; + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws IllegalValueException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new IllegalValueException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses a {@code Optional name} into an {@code Optional} if {@code name} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseName(Optional name) throws IllegalValueException { + requireNonNull(name); + return name.isPresent() ? Optional.of(new Name(name.get())) : Optional.empty(); + } + + /** + * Parses a {@code Optional phone} into an {@code Optional} if {@code phone} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parsePhone(Optional phone) throws IllegalValueException { + requireNonNull(phone); + return phone.isPresent() ? Optional.of(new Phone(phone.get())) : Optional.empty(); + } + + /** + * Parses a {@code Optional address} into an {@code Optional
} if {@code address} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional
parseAddress(Optional address) throws IllegalValueException { + requireNonNull(address); + return address.isPresent() ? Optional.of(new Address(address.get())) : Optional.empty(); + } + /** + * Parses a {@code Optional email} into an {@code Optional} if {@code email} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseEmail(Optional email) throws IllegalValueException { + requireNonNull(email); + return email.isPresent() ? Optional.of(new Email(email.get())) : Optional.empty(); + } + /** + * Parses a {@code Optional dob} into an {@code Optional} if {@code dob} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseDateOfBirth(Optional dob) throws IllegalValueException { + requireNonNull(dob); + return dob.isPresent() ? Optional.of(new DateOfBirth(dob.get())) : Optional.empty(); + } + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws IllegalValueException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(new Tag(tagName)); + } + return tagSet; + } +} diff --git a/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java b/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java new file mode 100644 index 000000000000..0916df545a3b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.PartialFindCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.NameStartsWithKeywordsPredicate; + +/** + * Parses input arguments and creates a new PartialFindCommand object + */ +public class PartialFindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PartialFindCommand + * and returns an PartialFindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PartialFindCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PartialFindCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new PartialFindCommand(new NameStartsWithKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/address/logic/parser/Prefix.java new file mode 100644 index 000000000000..c859d5fa5db1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/Prefix.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +/** + * A prefix that marks the beginning of an argument in an arguments string. + * E.g. 't/' in 'add James t/ friend'. + */ +public class Prefix { + private final String prefix; + + public Prefix(String prefix) { + this.prefix = prefix; + } + + public String getPrefix() { + return prefix; + } + + public String toString() { + return getPrefix(); + } + + @Override + public int hashCode() { + return prefix == null ? 0 : prefix.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Prefix)) { + return false; + } + if (obj == this) { + return true; + } + + Prefix otherPrefix = (Prefix) obj; + return otherPrefix.getPrefix().equals(getPrefix()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java new file mode 100644 index 000000000000..bbcae8f4b588 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SelectCommand object + */ +public class SelectCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SelectCommand + * and returns an SelectCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SelectCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new SelectCommand(index); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/WhyCommandParser.java b/src/main/java/seedu/address/logic/parser/WhyCommandParser.java new file mode 100644 index 000000000000..e7b06e208016 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/WhyCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +//import static java.util.Objects.requireNonNull; +//import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +//import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.WhyCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * WhyCommandParser: Adapted from DeleteCommandParser due to similarities + */ +public class WhyCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ReasonCommand + * and returns an RemarkCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public WhyCommand parse(String args) throws ParseException { + /** + Parsing + */ + try { + Index index = ParserUtil.parseIndex(args); + return new WhyCommand(index); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, WhyCommand.MESSAGE_USAGE)); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java new file mode 100644 index 000000000000..158a1a54c1c5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java @@ -0,0 +1,17 @@ +package seedu.address.logic.parser.exceptions; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * Represents a parse error encountered by a parser. + */ +public class ParseException extends IllegalValueException { + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 298cc1b82ce8..eafc6f3b72ed 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -1,15 +1,23 @@ package seedu.address.model; +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + import javafx.collections.ObservableList; import seedu.address.model.person.Person; import seedu.address.model.person.ReadOnlyPerson; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; import seedu.address.model.tag.Tag; import seedu.address.model.tag.UniqueTagList; -import java.util.*; -import java.util.stream.Collectors; - /** * Wraps all data at the address-book level * Duplicates are not allowed (by .equals comparison) @@ -19,6 +27,13 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; private final UniqueTagList tags; + /* + * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ { persons = new UniquePersonList(); tags = new UniqueTagList(); @@ -27,58 +42,76 @@ public class AddressBook implements ReadOnlyAddressBook { public AddressBook() {} /** - * Persons and Tags are copied into this addressbook + * Creates an AddressBook using the Persons and Tags in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(toBeCopied.getUniquePersonList(), toBeCopied.getUniqueTagList()); - } - - /** - * Persons and Tags are copied into this addressbook - */ - public AddressBook(UniquePersonList persons, UniqueTagList tags) { - resetData(persons.getInternalList(), tags.getInternalList()); - } - - public static ReadOnlyAddressBook getEmptyAddressBook() { - return new AddressBook(); + this(); + resetData(toBeCopied); } -//// list overwrite operations + //// list overwrite operations - public ObservableList getPersons() { - return persons.getInternalList(); + public void setPersons(List persons) throws DuplicatePersonException { + this.persons.setPersons(persons); } - public void setPersons(List persons) { - this.persons.getInternalList().setAll(persons); - } - - public void setTags(Collection tags) { - this.tags.getInternalList().setAll(tags); - } - - public void resetData(Collection newPersons, Collection newTags) { - setPersons(newPersons.stream().map(Person::new).collect(Collectors.toList())); - setTags(newTags); + public void setTags(Set tags) { + this.tags.setTags(tags); } + /** + * Resets the existing data of this {@code AddressBook} with {@code newData}. + */ public void resetData(ReadOnlyAddressBook newData) { - resetData(newData.getPersonList(), newData.getTagList()); + requireNonNull(newData); + try { + setPersons(newData.getPersonList()); + } catch (DuplicatePersonException e) { + assert false : "AddressBooks should not have duplicate persons"; + } + + setTags(new HashSet<>(newData.getTagList())); + syncMasterTagListWith(persons); } -//// person-level operations + //// person-level operations /** * Adds a person to the address book. * Also checks the new person's tags and updates {@link #tags} with any new tags found, * and updates the Tag objects in the person to point to those in {@link #tags}. * - * @throws UniquePersonList.DuplicatePersonException if an equivalent person already exists. + * @throws DuplicatePersonException if an equivalent person already exists. */ - public void addPerson(Person p) throws UniquePersonList.DuplicatePersonException { - syncTagsWithMasterList(p); - persons.add(p); + public void addPerson(ReadOnlyPerson p) throws DuplicatePersonException { + Person newPerson = new Person(p); + syncMasterTagListWith(newPerson); + // TODO: the tags master list will be updated even though the below line fails. + // This can cause the tags master list to have additional tags that are not tagged to any person + // in the person list. + persons.add(newPerson); + } + + /** + * Replaces the given person {@code target} in the list with {@code editedReadOnlyPerson}. + * {@code AddressBook}'s tag list will be updated with the tags of {@code editedReadOnlyPerson}. + * + * @throws DuplicatePersonException if updating the person's details causes the person to be equivalent to + * another existing person in the list. + * @throws PersonNotFoundException if {@code target} could not be found in the list. + * + * @see #syncMasterTagListWith(Person) + */ + public void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedReadOnlyPerson) + throws DuplicatePersonException, PersonNotFoundException { + requireNonNull(editedReadOnlyPerson); + + Person editedPerson = new Person(editedReadOnlyPerson); + syncMasterTagListWith(editedPerson); + // TODO: the tags master list will be updated even though the below line fails. + // This can cause the tags master list to have additional tags that are not tagged to any person + // in the person list. + persons.setPerson(target, editedPerson); } /** @@ -86,73 +119,73 @@ public void addPerson(Person p) throws UniquePersonList.DuplicatePersonException * - exists in the master list {@link #tags} * - points to a Tag object in the master list */ - private void syncTagsWithMasterList(Person person) { - final UniqueTagList personTags = person.getTags(); + private void syncMasterTagListWith(Person person) { + final UniqueTagList personTags = new UniqueTagList(person.getTags()); tags.mergeFrom(personTags); // Create map with values = tag object references in the master list + // used for checking person tag references final Map masterTagObjects = new HashMap<>(); - for (Tag tag : tags) { - masterTagObjects.put(tag, tag); - } + tags.forEach(tag -> masterTagObjects.put(tag, tag)); - // Rebuild the list of person tags using references from the master list - final Set commonTagReferences = new HashSet<>(); - for (Tag tag : personTags) { - commonTagReferences.add(masterTagObjects.get(tag)); - } - person.setTags(new UniqueTagList(commonTagReferences)); + // Rebuild the list of person tags to point to the relevant tags in the master tag list. + final Set correctTagReferences = new HashSet<>(); + personTags.forEach(tag -> correctTagReferences.add(masterTagObjects.get(tag))); + person.setTags(correctTagReferences); } - public boolean removePerson(ReadOnlyPerson key) throws UniquePersonList.PersonNotFoundException { + /** + * Ensures that every tag in these persons: + * - exists in the master list {@link #tags} + * - points to a Tag object in the master list + * @see #syncMasterTagListWith(Person) + */ + private void syncMasterTagListWith(UniquePersonList persons) { + persons.forEach(this::syncMasterTagListWith); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * @throws PersonNotFoundException if the {@code key} is not in this {@code AddressBook}. + */ + public boolean removePerson(ReadOnlyPerson key) throws PersonNotFoundException { if (persons.remove(key)) { return true; } else { - throw new UniquePersonList.PersonNotFoundException(); + throw new PersonNotFoundException(); } } -//// tag-level operations + //// tag-level operations public void addTag(Tag t) throws UniqueTagList.DuplicateTagException { tags.add(t); } -//// util methods + //// util methods @Override public String toString() { - return persons.getInternalList().size() + " persons, " + tags.getInternalList().size() + " tags"; + return persons.asObservableList().size() + " persons, " + tags.asObservableList().size() + " tags"; // TODO: refine later } @Override - public List getPersonList() { - return Collections.unmodifiableList(persons.getInternalList()); - } - - @Override - public List getTagList() { - return Collections.unmodifiableList(tags.getInternalList()); + public ObservableList getPersonList() { + return persons.asObservableList(); } @Override - public UniquePersonList getUniquePersonList() { - return this.persons; + public ObservableList getTagList() { + return tags.asObservableList(); } - @Override - public UniqueTagList getUniqueTagList() { - return this.tags; - } - - @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof AddressBook // instanceof handles nulls && this.persons.equals(((AddressBook) other).persons) - && this.tags.equals(((AddressBook) other).tags)); + && this.tags.equalsOrderInsensitive(((AddressBook) other).tags)); } @Override diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d14a27a93b5e..9c6aa4e766a0 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,16 +1,19 @@ package seedu.address.model; -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; +import java.util.function.Predicate; -import java.util.Set; +import javafx.collections.ObservableList; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * The API of the Model component. */ public interface Model { + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** Clears existing backing model and replaces with the provided new data. */ void resetData(ReadOnlyAddressBook newData); @@ -18,18 +21,28 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** Deletes the given person. */ - void deletePerson(ReadOnlyPerson target) throws UniquePersonList.PersonNotFoundException; + void deletePerson(ReadOnlyPerson target) throws PersonNotFoundException; /** Adds the given person */ - void addPerson(Person person) throws UniquePersonList.DuplicatePersonException; - - /** Returns the filtered person list as an {@code UnmodifiableObservableList} */ - UnmodifiableObservableList getFilteredPersonList(); - - /** Updates the filter of the filtered person list to show all persons */ - void updateFilteredListToShowAll(); - - /** Updates the filter of the filtered person list to filter by the given keywords*/ - void updateFilteredPersonList(Set keywords); + void addPerson(ReadOnlyPerson person) throws DuplicatePersonException; + + /** + * Replaces the given person {@code target} with {@code editedPerson}. + * + * @throws DuplicatePersonException if updating the person's details causes the person to be equivalent to + * another existing person in the list. + * @throws PersonNotFoundException if {@code target} could not be found in the list. + */ + void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson) + throws DuplicatePersonException, PersonNotFoundException; + + /** Returns an unmodifiable view of the filtered person list */ + ObservableList getFilteredPersonList(); + + /** + * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredPersonList(Predicate predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 869226d02bf1..095c831cfbf8 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -1,18 +1,20 @@ package seedu.address.model; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.function.Predicate; +import java.util.logging.Logger; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.UnmodifiableObservableList; -import seedu.address.commons.util.StringUtil; import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.core.ComponentManager; -import seedu.address.model.person.Person; import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; -import seedu.address.model.person.UniquePersonList.PersonNotFoundException; - -import java.util.Set; -import java.util.logging.Logger; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * Represents the in-memory model of the address book data. @@ -22,32 +24,25 @@ public class ModelManager extends ComponentManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final AddressBook addressBook; - private final FilteredList filteredPersons; + private final FilteredList filteredPersons; /** - * Initializes a ModelManager with the given AddressBook - * AddressBook and its variables should not be null + * Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(AddressBook src, UserPrefs userPrefs) { + public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { super(); - assert src != null; - assert userPrefs != null; + requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + src + " and user prefs " + userPrefs); + logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - addressBook = new AddressBook(src); - filteredPersons = new FilteredList<>(addressBook.getPersons()); + this.addressBook = new AddressBook(addressBook); + filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); } public ModelManager() { this(new AddressBook(), new UserPrefs()); } - public ModelManager(ReadOnlyAddressBook initialData, UserPrefs userPrefs) { - addressBook = new AddressBook(initialData); - filteredPersons = new FilteredList<>(addressBook.getPersons()); - } - @Override public void resetData(ReadOnlyAddressBook newData) { addressBook.resetData(newData); @@ -71,83 +66,54 @@ public synchronized void deletePerson(ReadOnlyPerson target) throws PersonNotFou } @Override - public synchronized void addPerson(Person person) throws UniquePersonList.DuplicatePersonException { + public synchronized void addPerson(ReadOnlyPerson person) throws DuplicatePersonException { addressBook.addPerson(person); - updateFilteredListToShowAll(); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); indicateAddressBookChanged(); } - //=========== Filtered Person List Accessors =============================================================== - - @Override - public UnmodifiableObservableList getFilteredPersonList() { - return new UnmodifiableObservableList<>(filteredPersons); - } - @Override - public void updateFilteredListToShowAll() { - filteredPersons.setPredicate(null); - } - - @Override - public void updateFilteredPersonList(Set keywords){ - updateFilteredPersonList(new PredicateExpression(new NameQualifier(keywords))); - } + public void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson) + throws DuplicatePersonException, PersonNotFoundException { + requireAllNonNull(target, editedPerson); - private void updateFilteredPersonList(Expression expression) { - filteredPersons.setPredicate(expression::satisfies); - } - - //========== Inner classes/interfaces used for filtering ================================================== - - interface Expression { - boolean satisfies(ReadOnlyPerson person); - String toString(); + addressBook.updatePerson(target, editedPerson); + indicateAddressBookChanged(); } - private class PredicateExpression implements Expression { - - private final Qualifier qualifier; - - PredicateExpression(Qualifier qualifier) { - this.qualifier = qualifier; - } - - @Override - public boolean satisfies(ReadOnlyPerson person) { - return qualifier.run(person); - } + //=========== Filtered Person List Accessors ============================================================= - @Override - public String toString() { - return qualifier.toString(); - } + /** + * Returns an unmodifiable view of the list of {@code ReadOnlyPerson} backed by the internal list of + * {@code addressBook} + */ + @Override + public ObservableList getFilteredPersonList() { + return FXCollections.unmodifiableObservableList(filteredPersons); } - interface Qualifier { - boolean run(ReadOnlyPerson person); - String toString(); + @Override + public void updateFilteredPersonList(Predicate predicate) { + requireNonNull(predicate); + filteredPersons.setPredicate(predicate); } - private class NameQualifier implements Qualifier { - private Set nameKeyWords; - - NameQualifier(Set nameKeyWords) { - this.nameKeyWords = nameKeyWords; + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; } - @Override - public boolean run(ReadOnlyPerson person) { - return nameKeyWords.stream() - .filter(keyword -> StringUtil.containsIgnoreCase(person.getName().fullName, keyword)) - .findAny() - .isPresent(); + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; } - @Override - public String toString() { - return "name=" + String.join(", ", nameKeyWords); - } + // state check + ModelManager other = (ModelManager) obj; + return addressBook.equals(other.addressBook) + && filteredPersons.equals(other.filteredPersons); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index bfca099b1e81..df24c7d9e928 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,30 +1,24 @@ package seedu.address.model; - +import javafx.collections.ObservableList; import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import java.util.List; /** * Unmodifiable view of an address book */ public interface ReadOnlyAddressBook { - UniqueTagList getUniqueTagList(); - - UniquePersonList getUniquePersonList(); - /** - * Returns an unmodifiable view of persons list + * Returns an unmodifiable view of the persons list. + * This list will not contain any duplicate persons. */ - List getPersonList(); + ObservableList getPersonList(); /** - * Returns an unmodifiable view of tags list + * Returns an unmodifiable view of the tags list. + * This list will not contain any duplicate tags. */ - List getTagList(); + ObservableList getTagList(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index da9c8037f495..8c8a071876eb 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -1,15 +1,21 @@ package seedu.address.model; -import seedu.address.commons.core.GuiSettings; - import java.util.Objects; +import seedu.address.commons.core.GuiSettings; + /** * Represents User's preferences. */ public class UserPrefs { - public GuiSettings guiSettings; + private GuiSettings guiSettings; + private String addressBookFilePath = "data/addressbook.xml"; + private String addressBookName = "MyAddressBook"; + + public UserPrefs() { + this.setGuiSettings(500, 500, 0, 0); + } public GuiSettings getGuiSettings() { return guiSettings == null ? new GuiSettings() : guiSettings; @@ -19,36 +25,54 @@ public void updateLastUsedGuiSetting(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public UserPrefs(){ - this.setGuiSettings(500, 500, 0, 0); - } - public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } + public String getAddressBookFilePath() { + return addressBookFilePath; + } + + public void setAddressBookFilePath(String addressBookFilePath) { + this.addressBookFilePath = addressBookFilePath; + } + + public String getAddressBookName() { + return addressBookName; + } + + public void setAddressBookName(String addressBookName) { + this.addressBookName = addressBookName; + } + @Override public boolean equals(Object other) { - if (other == this){ + if (other == this) { return true; } - if (!(other instanceof UserPrefs)){ //this handles null as well. + if (!(other instanceof UserPrefs)) { //this handles null as well. return false; } - UserPrefs o = (UserPrefs)other; + UserPrefs o = (UserPrefs) other; - return Objects.equals(guiSettings, o.guiSettings); + return Objects.equals(guiSettings, o.guiSettings) + && Objects.equals(addressBookFilePath, o.addressBookFilePath) + && Objects.equals(addressBookName, o.addressBookName); } @Override public int hashCode() { - return Objects.hash(guiSettings); + return Objects.hash(guiSettings, addressBookFilePath, addressBookName); } @Override - public String toString(){ - return guiSettings.toString(); + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Gui Settings : " + guiSettings.toString()); + sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nAddressBook name : " + addressBookName); + return sb.toString(); } } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index a2bd109c005e..f284e1373ac9 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -1,5 +1,6 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; import seedu.address.commons.exceptions.IllegalValueException; @@ -8,19 +9,33 @@ * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { - - public static final String MESSAGE_ADDRESS_CONSTRAINTS = "Person addresses can be in any format"; - public static final String ADDRESS_VALIDATION_REGEX = ".+"; + + public static final String MESSAGE_ADDRESS_CONSTRAINTS = + "Person addresses can take any values, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String ADDRESS_VALIDATION_REGEX = "[^\\s].*"; public final String value; + /** + * Initialise a Address object with value of empty String. This can ONLY be used in the default field of + * {@code AddPersonOptionalFieldDescriptor} + */ + public Address() { + this.value = ""; + } + /** * Validates given address. * * @throws IllegalValueException if given address string is invalid. */ public Address(String address) throws IllegalValueException { - assert address != null; + requireNonNull(address); if (!isValidAddress(address)) { throw new IllegalValueException(MESSAGE_ADDRESS_CONSTRAINTS); } @@ -51,4 +66,4 @@ public int hashCode() { return value.hashCode(); } -} \ No newline at end of file +} diff --git a/src/main/java/seedu/address/model/person/DateOfBirth.java b/src/main/java/seedu/address/model/person/DateOfBirth.java new file mode 100644 index 000000000000..040e1cd87ecb --- /dev/null +++ b/src/main/java/seedu/address/model/person/DateOfBirth.java @@ -0,0 +1,66 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * Represents a Person's date of birth in the address book. + */ +public class DateOfBirth { + + public static final String MESSAGE_DOB_CONSTRAINTS = + "Person's date of birth should only contain numeric characters and spaces, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String DOB_VALIDATION_REGEX = "[0-9][0-9]\\s+[0-9][0-9]\\s+[0-9][0-9][0-9][0-9].*"; + + public final String finalDateOfBirth; + + /** + * Initialise a DateOfBirth object with value of empty String. This can ONLY be used in the default field of + * {@code AddPersonOptionalFieldDescriptor} + */ + public DateOfBirth() { + this.finalDateOfBirth = ""; + } + + /** + * Validates given Date of Birth. + * + * @throws IllegalValueException if given date of birth string is invalid. + */ + public DateOfBirth(String dob) throws IllegalValueException { + requireNonNull(dob); + if (!isValidDateOfBirth(dob)) { + throw new IllegalValueException(MESSAGE_DOB_CONSTRAINTS); + } + this.finalDateOfBirth = dob; + } + + /** + * Returns true if a given string is a valid person date of birth. + */ + public static boolean isValidDateOfBirth(String test) { + return test.matches(DOB_VALIDATION_REGEX); + } + @Override + public String toString() { + return finalDateOfBirth; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateOfBirth // instanceof handles nulls + && this.finalDateOfBirth.equals(((DateOfBirth) other).finalDateOfBirth)); // state check + } + + @Override + public int hashCode() { + return finalDateOfBirth.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index 5da4d1078236..13572b176466 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -1,5 +1,6 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; import seedu.address.commons.exceptions.IllegalValueException; @@ -15,18 +16,26 @@ public class Email { public final String value; + /** + * Initialise a Email object with value of empty String. This can ONLY be used in the default field of + * {@code AddPersonOptionalFieldDescriptor} + */ + public Email() { + this.value = ""; + } + /** * Validates given email. * * @throws IllegalValueException if given email address string is invalid. */ public Email(String email) throws IllegalValueException { - assert email != null; - email = email.trim(); - if (!isValidEmail(email)) { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!isValidEmail(trimmedEmail)) { throw new IllegalValueException(MESSAGE_EMAIL_CONSTRAINTS); } - this.value = email; + this.value = trimmedEmail; } /** diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 4f30033e70fe..5d138e8ba223 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -1,5 +1,7 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; + import seedu.address.commons.exceptions.IllegalValueException; /** @@ -8,8 +10,14 @@ */ public class Name { - public static final String MESSAGE_NAME_CONSTRAINTS = "Person names should be spaces or alphanumeric characters"; - public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum} ]+"; + public static final String MESSAGE_NAME_CONSTRAINTS = + "Person names should only contain alphanumeric characters and spaces, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; public final String fullName; @@ -19,12 +27,12 @@ public class Name { * @throws IllegalValueException if given name string is invalid. */ public Name(String name) throws IllegalValueException { - assert name != null; - name = name.trim(); - if (!isValidName(name)) { + requireNonNull(name); + String trimmedName = name.trim(); + if (!isValidName(trimmedName)) { throw new IllegalValueException(MESSAGE_NAME_CONSTRAINTS); } - this.fullName = name; + this.fullName = trimmedName; } /** diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java new file mode 100644 index 000000000000..9d73fd178b0f --- /dev/null +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code ReadOnlyPerson}'s {@code Name} matches any of the keywords given. + */ +public class NameContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public NameContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(ReadOnlyPerson person) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java new file mode 100644 index 000000000000..38c64de4684e --- /dev/null +++ b/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java @@ -0,0 +1,29 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code ReadOnlyPerson}'s {@code Name} matches any of the keywords given. + */ +public class NameStartsWithKeywordsPredicate implements Predicate { + private final List keywords; + + public NameStartsWithKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(ReadOnlyPerson person) { + return keywords.stream() + .anyMatch(keyword -> person.getName().fullName.toLowerCase().startsWith(keyword.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NameStartsWithKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((NameStartsWithKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 03ffce7d2e79..8725c9991721 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -1,9 +1,16 @@ package seedu.address.model.person; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.model.tag.UniqueTagList; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.commands.WhyCommand.SHOWING_WHY_MESSAGE; import java.util.Objects; +import java.util.Set; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagList; /** * Represents a Person in the address book. @@ -11,62 +18,134 @@ */ public class Person implements ReadOnlyPerson { - private Name name; - private Phone phone; - private Email email; - private Address address; + private ObjectProperty name; + private ObjectProperty phone; + private ObjectProperty email; + private ObjectProperty
address; + private ObjectProperty dob; - private UniqueTagList tags; + private String reason; + + private ObjectProperty tags; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, UniqueTagList tags) { - assert !CollectionUtil.isAnyNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags = new UniqueTagList(tags); // protect internal tags from changes in the arg list + public Person(Name name, Phone phone, Email email, Address address, DateOfBirth dob, Set tags) { + requireAllNonNull(name, phone, email, address, dob, tags); + this.name = new SimpleObjectProperty<>(name); + this.phone = new SimpleObjectProperty<>(phone); + this.email = new SimpleObjectProperty<>(email); + this.address = new SimpleObjectProperty<>(address); + this.dob = new SimpleObjectProperty<>(dob); + // protect internal tags from changes in the arg list + this.tags = new SimpleObjectProperty<>(new UniqueTagList(tags)); } /** - * Copy constructor. + * Creates a copy of the given ReadOnlyPerson. */ public Person(ReadOnlyPerson source) { - this(source.getName(), source.getPhone(), source.getEmail(), source.getAddress(), source.getTags()); + this(source.getName(), source.getPhone(), source.getEmail(), source.getAddress(), + source.getDateOfBirth(), source.getTags()); + } + + public void setName(Name name) { + this.name.set(requireNonNull(name)); } @Override - public Name getName() { + public ObjectProperty nameProperty() { return name; } @Override - public Phone getPhone() { + public Name getName() { + return name.get(); + } + + public void setPhone(Phone phone) { + this.phone.set(requireNonNull(phone)); + } + + @Override + public ObjectProperty phoneProperty() { return phone; } @Override - public Email getEmail() { + public Phone getPhone() { + return phone.get(); + } + + public void setEmail(Email email) { + this.email.set(requireNonNull(email)); + } + + @Override + public ObjectProperty emailProperty() { return email; } @Override - public Address getAddress() { + public Email getEmail() { + return email.get(); + } + + public void setAddress(Address address) { + this.address.set(requireNonNull(address)); + } + + @Override + public ObjectProperty
addressProperty() { return address; } @Override - public UniqueTagList getTags() { - return new UniqueTagList(tags); + public Address getAddress() { + return address.get(); + } + + + public void setDateOfBirth(DateOfBirth dob) { + this.dob.set(requireNonNull(dob)); + } + + @Override + public ObjectProperty dobProperty() { + return dob; + } + + @Override + public DateOfBirth getDateOfBirth() { + return dob.get(); + } + + public String getReason() { + Address a = this.getAddress(); + Name n = this.getName(); + this.reason = String.format(SHOWING_WHY_MESSAGE, n, a); + return reason; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + @Override + public Set getTags() { + return tags.get().toSet(); + } + + public ObjectProperty tagProperty() { + return tags; } /** - * Replaces this person's tags with the tags in the argument tag list. + * Replaces this person's tags with the tags in the argument tag set. */ - public void setTags(UniqueTagList replacement) { - tags.setTags(replacement); + public void setTags(Set replacement) { + tags.set(new UniqueTagList(replacement)); } @Override @@ -79,7 +158,7 @@ public boolean equals(Object other) { @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, dob, tags); } @Override diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d27b2244b727..dd40b4d4cc40 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -1,5 +1,7 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; + import seedu.address.commons.exceptions.IllegalValueException; /** @@ -8,23 +10,32 @@ */ public class Phone { - public static final String MESSAGE_PHONE_CONSTRAINTS = "Person phone numbers should only contain numbers"; - public static final String PHONE_VALIDATION_REGEX = "\\d+"; + public static final String MESSAGE_PHONE_CONSTRAINTS = + "Phone numbers can only contain numbers, and should be at least 3 digits long"; + public static final String PHONE_VALIDATION_REGEX = "\\d{3,}"; public final String value; + /** + * Initialise a Phone object with value of empty String. This can ONLY be used in the default field of + * {@code AddPersonOptionalFieldDescriptor} + */ + public Phone() { + this.value = ""; + } + /** * Validates given phone number. * * @throws IllegalValueException if given phone string is invalid. */ public Phone(String phone) throws IllegalValueException { - assert phone != null; - phone = phone.trim(); - if (!isValidPhone(phone)) { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!isValidPhone(trimmedPhone)) { throw new IllegalValueException(MESSAGE_PHONE_CONSTRAINTS); } - this.value = phone; + this.value = trimmedPhone; } /** diff --git a/src/main/java/seedu/address/model/person/ReadOnlyPerson.java b/src/main/java/seedu/address/model/person/ReadOnlyPerson.java index d45be4b5fe36..4fffb5d8a77c 100644 --- a/src/main/java/seedu/address/model/person/ReadOnlyPerson.java +++ b/src/main/java/seedu/address/model/person/ReadOnlyPerson.java @@ -1,5 +1,9 @@ package seedu.address.model.person; +import java.util.Set; + +import javafx.beans.property.ObjectProperty; +import seedu.address.model.tag.Tag; import seedu.address.model.tag.UniqueTagList; /** @@ -8,16 +12,21 @@ */ public interface ReadOnlyPerson { + ObjectProperty nameProperty(); Name getName(); + ObjectProperty phoneProperty(); Phone getPhone(); + ObjectProperty emailProperty(); Email getEmail(); + ObjectProperty
addressProperty(); Address getAddress(); + ObjectProperty dobProperty(); + DateOfBirth getDateOfBirth(); - /** - * The returned TagList is a deep copy of the internal TagList, - * changes on the returned list will not affect the person's internal tags. - */ - UniqueTagList getTags(); + String getReason(); + + ObjectProperty tagProperty(); + Set getTags(); /** * Returns true if both have the same state. (interfaces cannot override .equals) @@ -28,7 +37,8 @@ default boolean isSameStateAs(ReadOnlyPerson other) { && other.getName().equals(this.getName()) // state checks here onwards && other.getPhone().equals(this.getPhone()) && other.getEmail().equals(this.getEmail()) - && other.getAddress().equals(this.getAddress())); + && other.getAddress().equals(this.getAddress()) + && other.getDateOfBirth().equals(this.getDateOfBirth())); } /** @@ -43,23 +53,11 @@ default String getAsText() { .append(getEmail()) .append(" Address: ") .append(getAddress()) + .append(" DateOfBirth: ") + .append(getDateOfBirth()) .append(" Tags: "); getTags().forEach(builder::append); return builder.toString(); } - /** - * Returns a string representation of this Person's tags - */ - default String tagsString() { - final StringBuffer buffer = new StringBuffer(); - final String separator = ", "; - getTags().forEach(tag -> buffer.append(tag).append(separator)); - if (buffer.length() == 0) { - return ""; - } else { - return buffer.substring(0, buffer.length() - separator.length()); - } - } - } diff --git a/src/main/java/seedu/address/model/person/Reason.java b/src/main/java/seedu/address/model/person/Reason.java new file mode 100644 index 000000000000..ea8e8a23134e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Reason.java @@ -0,0 +1,61 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * Represents a Person's reason for "why" in the address book. + */ +public class Reason { + + public static final String SHOWING_WHY_MESSAGE = "Because %1$s lives in %2$s"; + public static final String MESSAGE_ADDRESS_CONSTRAINTS = + "Person reason can take any values, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String ADDRESS_VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Validates given address. + * + * @throws IllegalValueException if given address string is invalid. + */ + public Reason(String reason) throws IllegalValueException { + requireNonNull(reason); + if (!isValidReason(reason)) { + throw new IllegalValueException(MESSAGE_ADDRESS_CONSTRAINTS); + } + this.value = reason; + } + + /** + * Returns true if a given string is a valid person email. + */ + public static boolean isValidReason(String test) { + return test.matches(ADDRESS_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Address // instanceof handles nulls + && this.value.equals(((Address) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index 263f1fcc7dd5..3b293a9d2b80 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -1,11 +1,17 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; + +import org.fxmisc.easybind.EasyBind; + import javafx.collections.FXCollections; import javafx.collections.ObservableList; import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.exceptions.DuplicateDataException; - -import java.util.*; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * A list of persons that enforces uniqueness between its elements and does not allow nulls. @@ -17,33 +23,15 @@ */ public class UniquePersonList implements Iterable { - /** - * Signals that an operation would have violated the 'no duplicates' property of the list. - */ - public static class DuplicatePersonException extends DuplicateDataException { - protected DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } - } - - /** - * Signals that an operation targeting a specified person in the list would fail because - * there is no such matching person in the list. - */ - public static class PersonNotFoundException extends Exception {} - private final ObservableList internalList = FXCollections.observableArrayList(); - - /** - * Constructs empty PersonList. - */ - public UniquePersonList() {} + // used by asObservableList() + private final ObservableList mappedList = EasyBind.map(internalList, (person) -> person); /** * Returns true if the list contains an equivalent person as the given argument. */ public boolean contains(ReadOnlyPerson toCheck) { - assert toCheck != null; + requireNonNull(toCheck); return internalList.contains(toCheck); } @@ -52,12 +40,34 @@ public boolean contains(ReadOnlyPerson toCheck) { * * @throws DuplicatePersonException if the person to add is a duplicate of an existing person in the list. */ - public void add(Person toAdd) throws DuplicatePersonException { - assert toAdd != null; + public void add(ReadOnlyPerson toAdd) throws DuplicatePersonException { + requireNonNull(toAdd); if (contains(toAdd)) { throw new DuplicatePersonException(); } - internalList.add(toAdd); + internalList.add(new Person(toAdd)); + } + + /** + * Replaces the person {@code target} in the list with {@code editedPerson}. + * + * @throws DuplicatePersonException if the replacement is equivalent to another existing person in the list. + * @throws PersonNotFoundException if {@code target} could not be found in the list. + */ + public void setPerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson) + throws DuplicatePersonException, PersonNotFoundException { + requireNonNull(editedPerson); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new PersonNotFoundException(); + } + + if (!target.equals(editedPerson) && internalList.contains(editedPerson)) { + throw new DuplicatePersonException(); + } + + internalList.set(index, new Person(editedPerson)); } /** @@ -66,7 +76,7 @@ public void add(Person toAdd) throws DuplicatePersonException { * @throws PersonNotFoundException if no such person could be found in the list. */ public boolean remove(ReadOnlyPerson toRemove) throws PersonNotFoundException { - assert toRemove != null; + requireNonNull(toRemove); final boolean personFoundAndDeleted = internalList.remove(toRemove); if (!personFoundAndDeleted) { throw new PersonNotFoundException(); @@ -74,8 +84,23 @@ public boolean remove(ReadOnlyPerson toRemove) throws PersonNotFoundException { return personFoundAndDeleted; } - public ObservableList getInternalList() { - return internalList; + public void setPersons(UniquePersonList replacement) { + this.internalList.setAll(replacement.internalList); + } + + public void setPersons(List persons) throws DuplicatePersonException { + final UniquePersonList replacement = new UniquePersonList(); + for (final ReadOnlyPerson person : persons) { + replacement.add(new Person(person)); + } + setPersons(replacement); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asObservableList() { + return FXCollections.unmodifiableObservableList(mappedList); } @Override @@ -87,8 +112,7 @@ public Iterator iterator() { public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof UniquePersonList // instanceof handles nulls - && this.internalList.equals( - ((UniquePersonList) other).internalList)); + && this.internalList.equals(((UniquePersonList) other).internalList)); } @Override diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java new file mode 100644 index 000000000000..fce401885dc8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java @@ -0,0 +1,12 @@ +package seedu.address.model.person.exceptions; + +import seedu.address.commons.exceptions.DuplicateDataException; + +/** + * Signals that the operation will result in duplicate Person objects. + */ +public class DuplicatePersonException extends DuplicateDataException { + public DuplicatePersonException() { + super("Operation would result in duplicate persons"); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java new file mode 100644 index 000000000000..f757e25f5566 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation is unable to find the specified person. + */ +public class PersonNotFoundException extends Exception {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index 5bcffdb5ddf1..9d1a5b619fc0 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -1,5 +1,6 @@ package seedu.address.model.tag; +import static java.util.Objects.requireNonNull; import seedu.address.commons.exceptions.IllegalValueException; @@ -12,10 +13,7 @@ public class Tag { public static final String MESSAGE_TAG_CONSTRAINTS = "Tags names should be alphanumeric"; public static final String TAG_VALIDATION_REGEX = "\\p{Alnum}+"; - public String tagName; - - public Tag() { - } + public final String tagName; /** * Validates given tag name. @@ -23,12 +21,12 @@ public Tag() { * @throws IllegalValueException if the given tag name string is invalid. */ public Tag(String name) throws IllegalValueException { - assert name != null; - name = name.trim(); - if (!isValidTagName(name)) { + requireNonNull(name); + String trimmedName = name.trim(); + if (!isValidTagName(trimmedName)) { throw new IllegalValueException(MESSAGE_TAG_CONSTRAINTS); } - this.tagName = name; + this.tagName = trimmedName; } /** @@ -57,4 +55,7 @@ public String toString() { return '[' + tagName + ']'; } + public String getTagName() { + return tagName; + } } diff --git a/src/main/java/seedu/address/model/tag/UniqueTagList.java b/src/main/java/seedu/address/model/tag/UniqueTagList.java index 76fb7ff3dc5d..e9a74947fc3f 100644 --- a/src/main/java/seedu/address/model/tag/UniqueTagList.java +++ b/src/main/java/seedu/address/model/tag/UniqueTagList.java @@ -1,11 +1,16 @@ package seedu.address.model.tag; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import seedu.address.commons.util.CollectionUtil; import seedu.address.commons.exceptions.DuplicateDataException; - -import java.util.*; +import seedu.address.commons.util.CollectionUtil; /** * A list of tags that enforces no nulls and uniqueness between its elements. @@ -13,19 +18,9 @@ * Supports minimal set of list operations for the app's features. * * @see Tag#equals(Object) - * @see CollectionUtil#elementsAreUnique(Collection) */ public class UniqueTagList implements Iterable { - /** - * Signals that an operation would have violated the 'no duplicates' property of the list. - */ - public static class DuplicateTagException extends DuplicateDataException { - protected DuplicateTagException() { - super("Operation would result in duplicate tags"); - } - } - private final ObservableList internalList = FXCollections.observableArrayList(); /** @@ -34,75 +29,51 @@ protected DuplicateTagException() { public UniqueTagList() {} /** - * Varargs/array constructor, enforces no nulls or duplicates. - */ - public UniqueTagList(Tag... tags) throws DuplicateTagException { - assert !CollectionUtil.isAnyNull((Object[]) tags); - final List initialTags = Arrays.asList(tags); - if (!CollectionUtil.elementsAreUnique(initialTags)) { - throw new DuplicateTagException(); - } - internalList.addAll(initialTags); - } - - /** - * java collections constructor, enforces no null or duplicate elements. - */ - public UniqueTagList(Collection tags) throws DuplicateTagException { - CollectionUtil.assertNoNullElements(tags); - if (!CollectionUtil.elementsAreUnique(tags)) { - throw new DuplicateTagException(); - } - internalList.addAll(tags); - } - - /** - * java set constructor, enforces no nulls. + * Creates a UniqueTagList using given tags. + * Enforces no nulls. */ public UniqueTagList(Set tags) { - CollectionUtil.assertNoNullElements(tags); + requireAllNonNull(tags); internalList.addAll(tags); - } - /** - * Copy constructor, insulates from changes in source. - */ - public UniqueTagList(UniqueTagList source) { - internalList.addAll(source.internalList); // insulate internal list from changes in argument + assert CollectionUtil.elementsAreUnique(internalList); } /** - * All tags in this list as a Set. This set is mutable and change-insulated against the internal list. + * Returns all tags in this list as a Set. + * This set is mutable and change-insulated against the internal list. */ public Set toSet() { + assert CollectionUtil.elementsAreUnique(internalList); return new HashSet<>(internalList); } /** * Replaces the Tags in this list with those in the argument tag list. */ - public void setTags(UniqueTagList replacement) { - this.internalList.clear(); - this.internalList.addAll(replacement.internalList); + public void setTags(Set tags) { + requireAllNonNull(tags); + internalList.setAll(tags); + assert CollectionUtil.elementsAreUnique(internalList); } /** - * Adds every tag from the argument list that does not yet exist in this list. + * Ensures every tag in the argument list exists in this object. */ - public void mergeFrom(UniqueTagList tags) { + public void mergeFrom(UniqueTagList from) { final Set alreadyInside = this.toSet(); - for (Tag tag : tags) { - if (!alreadyInside.contains(tag)) { - internalList.add(tag); - } - } + from.internalList.stream() + .filter(tag -> !alreadyInside.contains(tag)) + .forEach(internalList::add); + + assert CollectionUtil.elementsAreUnique(internalList); } /** * Returns true if the list contains an equivalent Tag as the given argument. */ public boolean contains(Tag toCheck) { - assert toCheck != null; + requireNonNull(toCheck); return internalList.contains(toCheck); } @@ -112,32 +83,60 @@ public boolean contains(Tag toCheck) { * @throws DuplicateTagException if the Tag to add is a duplicate of an existing Tag in the list. */ public void add(Tag toAdd) throws DuplicateTagException { - assert toAdd != null; + requireNonNull(toAdd); if (contains(toAdd)) { throw new DuplicateTagException(); } internalList.add(toAdd); + + assert CollectionUtil.elementsAreUnique(internalList); } @Override public Iterator iterator() { + assert CollectionUtil.elementsAreUnique(internalList); return internalList.iterator(); } - public ObservableList getInternalList() { - return internalList; + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asObservableList() { + assert CollectionUtil.elementsAreUnique(internalList); + return FXCollections.unmodifiableObservableList(internalList); } @Override public boolean equals(Object other) { + assert CollectionUtil.elementsAreUnique(internalList); return other == this // short circuit if same object || (other instanceof UniqueTagList // instanceof handles nulls - && this.internalList.equals( - ((UniqueTagList) other).internalList)); + && this.internalList.equals(((UniqueTagList) other).internalList)); + } + + /** + * Returns true if the element in this list is equal to the elements in {@code other}. + * The elements do not have to be in the same order. + */ + public boolean equalsOrderInsensitive(UniqueTagList other) { + assert CollectionUtil.elementsAreUnique(internalList); + assert CollectionUtil.elementsAreUnique(other.internalList); + return this == other || new HashSet<>(this.internalList).equals(new HashSet<>(other.internalList)); } @Override public int hashCode() { + assert CollectionUtil.elementsAreUnique(internalList); return internalList.hashCode(); } + + /** + * Signals that an operation would have violated the 'no duplicates' property of the list. + */ + public static class DuplicateTagException extends DuplicateDataException { + protected DuplicateTagException() { + super("Operation would result in duplicate tags"); + } + } + } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java new file mode 100644 index 000000000000..c8787394cf61 --- /dev/null +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -0,0 +1,73 @@ +package seedu.address.model.util; + +import java.util.HashSet; +import java.util.Set; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.AddressBook; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.tag.Tag; + +/** + * Contains utility methods for populating {@code AddressBook} with sample data. + */ +public class SampleDataUtil { + public static Person[] getSamplePersons() { + try { + return new Person[] { + new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), + new Address("Blk 30 Geylang Street 29, #06-40"), + new DateOfBirth("27 08 1997"), getTagSet("friends")), + new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + new DateOfBirth("28 01 1997"), getTagSet("colleagues", "friends")), + new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + new DateOfBirth("29 01 1997"), getTagSet("neighbours")), + new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new DateOfBirth("30 01 1997"), getTagSet("family")), + new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), + new DateOfBirth("31 01 1997"), getTagSet("classmates")), + new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), + new DateOfBirth("01 02 1997"), getTagSet("colleagues")) + }; + } catch (IllegalValueException e) { + throw new AssertionError("sample data cannot be invalid", e); + } + } + + public static ReadOnlyAddressBook getSampleAddressBook() { + try { + AddressBook sampleAb = new AddressBook(); + for (Person samplePerson : getSamplePersons()) { + sampleAb.addPerson(samplePerson); + } + return sampleAb; + } catch (DuplicatePersonException e) { + throw new AssertionError("sample data cannot contain duplicate persons", e); + } + } + + /** + * Returns a tag set containing the list of strings given. + */ + public static Set getTagSet(String... strings) throws IllegalValueException { + HashSet tags = new HashSet<>(); + for (String s : strings) { + tags.add(new Tag(s)); + } + + return tags; + } + +} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java index 80033086985b..0e2d53ad81dd 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/address/storage/AddressBookStorage.java @@ -1,11 +1,11 @@ package seedu.address.storage; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - import java.io.IOException; import java.util.Optional; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.ReadOnlyAddressBook; + /** * Represents a storage for {@link seedu.address.model.AddressBook}. */ @@ -16,6 +16,14 @@ public interface AddressBookStorage { */ String getAddressBookFilePath(); + /** + * Returns Backup AddressBook data as a {@link ReadOnlyAddressBook}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readBackupAddressBook() throws DataConversionException, IOException; + /** * Returns AddressBook data as a {@link ReadOnlyAddressBook}. * Returns {@code Optional.empty()} if storage file is not found. @@ -29,6 +37,13 @@ public interface AddressBookStorage { */ Optional readAddressBook(String filePath) throws DataConversionException, IOException; + /** + * Backups the given {@link ReadOnlyAddressBook} to the storage. + * @param addressBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + /** * Saves the given {@link ReadOnlyAddressBook} to the storage. * @param addressBook cannot be null. diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java index 1efa8288e4f6..4f41aff81251 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java @@ -1,36 +1,31 @@ package seedu.address.storage; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.UserPrefs; - -import java.io.File; import java.io.IOException; import java.util.Optional; -import java.util.logging.Logger; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file */ -public class JsonUserPrefsStorage implements UserPrefsStorage{ - - private static final Logger logger = LogsCenter.getLogger(JsonUserPrefsStorage.class); +public class JsonUserPrefsStorage implements UserPrefsStorage { private String filePath; - public JsonUserPrefsStorage(String filePath){ + public JsonUserPrefsStorage(String filePath) { this.filePath = filePath; } @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return readUserPrefs(filePath); + public String getUserPrefsFilePath() { + return filePath; } @Override - public void saveUserPrefs(UserPrefs userPrefs) throws IOException { - saveUserPrefs(userPrefs, filePath); + public Optional readUserPrefs() throws DataConversionException, IOException { + return readUserPrefs(filePath); } /** @@ -39,35 +34,12 @@ public void saveUserPrefs(UserPrefs userPrefs) throws IOException { * @throws DataConversionException if the file format is not as expected. */ public Optional readUserPrefs(String prefsFilePath) throws DataConversionException { - assert prefsFilePath != null; - - File prefsFile = new File(prefsFilePath); - - if (!prefsFile.exists()) { - logger.info("Prefs file " + prefsFile + " not found"); - return Optional.empty(); - } - - UserPrefs prefs; - - try { - prefs = FileUtil.deserializeObjectFromJsonFile(prefsFile, UserPrefs.class); - } catch (IOException e) { - logger.warning("Error reading from prefs file " + prefsFile + ": " + e); - throw new DataConversionException(e); - } - - return Optional.of(prefs); + return JsonUtil.readJsonFile(prefsFilePath, UserPrefs.class); } - /** - * Similar to {@link #saveUserPrefs(UserPrefs)} - * @param prefsFilePath location of the data. Cannot be null. - */ - public void saveUserPrefs(UserPrefs userPrefs, String prefsFilePath) throws IOException { - assert userPrefs != null; - assert prefsFilePath != null; - - FileUtil.serializeObjectToJsonFile(new File(prefsFilePath), userPrefs); + @Override + public void saveUserPrefs(UserPrefs userPrefs) throws IOException { + JsonUtil.saveJsonFile(userPrefs, filePath); } + } diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 91002a8a821a..c0881a5a6483 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -1,15 +1,14 @@ package seedu.address.storage; +import java.io.IOException; +import java.util.Optional; + import seedu.address.commons.events.model.AddressBookChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Optional; - /** * API of the Storage component */ diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index ba1f72f15c27..a4b8ce99e7f8 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -1,6 +1,11 @@ package seedu.address.storage; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Logger; + import com.google.common.eventbus.Subscribe; + import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; @@ -9,11 +14,6 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Optional; -import java.util.logging.Logger; - /** * Manages storage of AddressBook data in local storage. */ @@ -30,12 +30,13 @@ public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage us this.userPrefsStorage = userPrefsStorage; } - public StorageManager(String addressBookFilePath, String userPrefsFilePath) { - this(new XmlAddressBookStorage(addressBookFilePath), new JsonUserPrefsStorage(userPrefsFilePath)); - } - // ================ UserPrefs methods ============================== + @Override + public String getUserPrefsFilePath() { + return userPrefsStorage.getUserPrefsFilePath(); + } + @Override public Optional readUserPrefs() throws DataConversionException, IOException { return userPrefsStorage.readUserPrefs(); @@ -54,6 +55,11 @@ public String getAddressBookFilePath() { return addressBookStorage.getAddressBookFilePath(); } + @Override + public Optional readBackupAddressBook() throws DataConversionException, IOException { + return readAddressBook(addressBookStorage.getAddressBookFilePath() + "-backup.xml"); + } + @Override public Optional readAddressBook() throws DataConversionException, IOException { return readAddressBook(addressBookStorage.getAddressBookFilePath()); @@ -65,6 +71,11 @@ public Optional readAddressBook(String filePath) throws Dat return addressBookStorage.readAddressBook(filePath); } + @Override + public void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath() + "-backup.xml"); + } + @Override public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/address/storage/UserPrefsStorage.java index ad2dc935187c..146477fad976 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/UserPrefsStorage.java @@ -1,16 +1,21 @@ package seedu.address.storage; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.UserPrefs; - import java.io.IOException; import java.util.Optional; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.UserPrefs; + /** * Represents a storage for {@link seedu.address.model.UserPrefs}. */ public interface UserPrefsStorage { + /** + * Returns the file path of the UserPrefs data file. + */ + String getUserPrefsFilePath(); + /** * Returns UserPrefs data from storage. * Returns {@code Optional.empty()} if storage file is not found. diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java index f2167ec201b4..8d28c7d1a5a1 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java @@ -1,13 +1,21 @@ package seedu.address.storage; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.*; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; - -import javax.xml.bind.annotation.XmlElement; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Address; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.tag.Tag; /** * JAXB-friendly version of the Person. @@ -22,12 +30,15 @@ public class XmlAdaptedPerson { private String email; @XmlElement(required = true) private String address; + @XmlElement(required = true) + private String dob; @XmlElement private List tagged = new ArrayList<>(); /** - * No-arg constructor for JAXB use. + * Constructs an XmlAdaptedPerson. + * This is the no-arg constructor that is required by JAXB. */ public XmlAdaptedPerson() {} @@ -42,6 +53,7 @@ public XmlAdaptedPerson(ReadOnlyPerson source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + dob = source.getDateOfBirth().finalDateOfBirth; tagged = new ArrayList<>(); for (Tag tag : source.getTags()) { tagged.add(new XmlAdaptedTag(tag)); @@ -59,10 +71,11 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } final Name name = new Name(this.name); - final Phone phone = new Phone(this.phone); - final Email email = new Email(this.email); - final Address address = new Address(this.address); - final UniqueTagList tags = new UniqueTagList(personTags); - return new Person(name, phone, email, address, tags); + final Phone phone = this.phone.equals("") ? new Phone() : new Phone(this.phone); + final Email email = this.email.equals("") ? new Email() : new Email(this.email); + final Address address = this.address.equals("") ? new Address() : new Address(this.address); + final DateOfBirth dob = this.dob.equals("") ? new DateOfBirth() : new DateOfBirth(this.dob); + final Set tags = new HashSet<>(personTags); + return new Person(name, phone, email, address, dob, tags); } } diff --git a/src/main/java/seedu/address/storage/XmlAdaptedTag.java b/src/main/java/seedu/address/storage/XmlAdaptedTag.java index b9723fafbc67..aad2d96dde3f 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedTag.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedTag.java @@ -1,21 +1,21 @@ package seedu.address.storage; -import seedu.address.commons.util.CollectionUtil; +import javax.xml.bind.annotation.XmlValue; + import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.tag.Tag; -import javax.xml.bind.annotation.XmlValue; - /** * JAXB-friendly adapted version of the Tag. */ public class XmlAdaptedTag { @XmlValue - public String tagName; + private String tagName; /** - * No-arg constructor for JAXB use. + * Constructs an XmlAdaptedTag. + * This is the no-arg constructor that is required by JAXB. */ public XmlAdaptedTag() {} diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java index 30cb00270cc4..293e5e315a0e 100644 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java @@ -1,9 +1,6 @@ package seedu.address.storage; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.ReadOnlyAddressBook; +import static java.util.Objects.requireNonNull; import java.io.File; import java.io.FileNotFoundException; @@ -11,6 +8,11 @@ import java.util.Optional; import java.util.logging.Logger; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.ReadOnlyAddressBook; + /** * A class to access AddressBook data stored as an xml file on the hard disk. */ @@ -20,21 +22,34 @@ public class XmlAddressBookStorage implements AddressBookStorage { private String filePath; - public XmlAddressBookStorage(String filePath){ + public XmlAddressBookStorage(String filePath) { this.filePath = filePath; } - public String getAddressBookFilePath(){ + public String getAddressBookFilePath() { return filePath; } + @Override + public Optional readBackupAddressBook() throws DataConversionException, IOException { + requireNonNull(filePath); + + return readAddressBook(filePath + "-backup.xml"); + } + + @Override + public Optional readAddressBook() throws DataConversionException, IOException { + return readAddressBook(filePath); + } + /** * Similar to {@link #readAddressBook()} * @param filePath location of the data. Cannot be null * @throws DataConversionException if the file is not in the correct format. */ - public Optional readAddressBook(String filePath) throws DataConversionException, FileNotFoundException { - assert filePath != null; + public Optional readAddressBook(String filePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(filePath); File addressBookFile = new File(filePath); @@ -48,26 +63,29 @@ public Optional readAddressBook(String filePath) throws Dat return Optional.of(addressBookOptional); } + @Override + public void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + requireNonNull(filePath); + + saveAddressBook(addressBook, filePath + "-backup.xml"); + } + + @Override + public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + saveAddressBook(addressBook, filePath); + } + /** * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)} * @param filePath location of the data. Cannot be null */ public void saveAddressBook(ReadOnlyAddressBook addressBook, String filePath) throws IOException { - assert addressBook != null; - assert filePath != null; + requireNonNull(addressBook); + requireNonNull(filePath); File file = new File(filePath); FileUtil.createIfMissing(file); XmlFileStorage.saveDataToFile(file, new XmlSerializableAddressBook(addressBook)); } - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } } diff --git a/src/main/java/seedu/address/storage/XmlFileStorage.java b/src/main/java/seedu/address/storage/XmlFileStorage.java index 27a5210cadaf..ee1bb24be4e1 100644 --- a/src/main/java/seedu/address/storage/XmlFileStorage.java +++ b/src/main/java/seedu/address/storage/XmlFileStorage.java @@ -1,12 +1,13 @@ package seedu.address.storage; -import seedu.address.commons.util.XmlUtil; -import seedu.address.commons.exceptions.DataConversionException; - -import javax.xml.bind.JAXBException; import java.io.File; import java.io.FileNotFoundException; +import javax.xml.bind.JAXBException; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.XmlUtil; + /** * Stores addressbook data in an XML file */ diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java index b7ec533a3a1e..37eaee382ce4 100644 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java @@ -1,19 +1,19 @@ package seedu.address.storage; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; -import seedu.address.model.tag.UniqueTagList; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.model.person.UniquePersonList; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.model.tag.Tag; + /** * An Immutable AddressBook that is serializable to XML format */ @@ -23,66 +23,52 @@ public class XmlSerializableAddressBook implements ReadOnlyAddressBook { @XmlElement private List persons; @XmlElement - private List tags; + private List tags; - { + /** + * Creates an empty XmlSerializableAddressBook. + * This empty constructor is required for marshalling. + */ + public XmlSerializableAddressBook() { persons = new ArrayList<>(); tags = new ArrayList<>(); } - /** - * Empty constructor required for marshalling - */ - public XmlSerializableAddressBook() {} - /** * Conversion */ public XmlSerializableAddressBook(ReadOnlyAddressBook src) { + this(); persons.addAll(src.getPersonList().stream().map(XmlAdaptedPerson::new).collect(Collectors.toList())); - tags = src.getTagList(); - } - - @Override - public UniqueTagList getUniqueTagList() { - try { - return new UniqueTagList(tags); - } catch (UniqueTagList.DuplicateTagException e) { - //TODO: better error handling - e.printStackTrace(); - return null; - } + tags.addAll(src.getTagList().stream().map(XmlAdaptedTag::new).collect(Collectors.toList())); } @Override - public UniquePersonList getUniquePersonList() { - UniquePersonList lists = new UniquePersonList(); - for (XmlAdaptedPerson p : persons) { + public ObservableList getPersonList() { + final ObservableList persons = this.persons.stream().map(p -> { try { - lists.add(p.toModelType()); + return p.toModelType(); } catch (IllegalValueException e) { + e.printStackTrace(); //TODO: better error handling + return null; } - } - return lists; + }).collect(Collectors.toCollection(FXCollections::observableArrayList)); + return FXCollections.unmodifiableObservableList(persons); } @Override - public List getPersonList() { - return persons.stream().map(p -> { + public ObservableList getTagList() { + final ObservableList tags = this.tags.stream().map(t -> { try { - return p.toModelType(); + return t.toModelType(); } catch (IllegalValueException e) { e.printStackTrace(); //TODO: better error handling return null; } - }).collect(Collectors.toCollection(ArrayList::new)); - } - - @Override - public List getTagList() { - return Collections.unmodifiableList(tags); + }).collect(Collectors.toCollection(FXCollections::observableArrayList)); + return FXCollections.unmodifiableObservableList(tags); } } diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java index 54b88318019b..98689931eb17 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/address/ui/BrowserPanel.java @@ -1,61 +1,61 @@ package seedu.address.ui; +import java.net.URL; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; import javafx.event.Event; -import javafx.scene.Node; -import javafx.scene.layout.AnchorPane; +import javafx.fxml.FXML; +import javafx.scene.layout.Region; import javafx.scene.web.WebView; -import seedu.address.commons.util.FxViewUtil; -import seedu.address.model.person.ReadOnlyPerson; +import seedu.address.MainApp; import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; +import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; +import seedu.address.model.person.ReadOnlyPerson; /** * The Browser Panel of the App. */ -public class BrowserPanel extends UiPart{ +public class BrowserPanel extends UiPart { + + public static final String DEFAULT_PAGE = "default.html"; + public static final String GOOGLE_SEARCH_URL_PREFIX = "https://www.google.com.sg/search?safe=off&q="; + public static final String GOOGLE_SEARCH_URL_SUFFIX = "&cad=h"; + + private static final String FXML = "BrowserPanel.fxml"; - private static Logger logger = LogsCenter.getLogger(BrowserPanel.class); + private final Logger logger = LogsCenter.getLogger(this.getClass()); + + @FXML private WebView browser; - /** - * Constructor is kept private as {@link #load(AnchorPane)} is the only way to create a BrowserPanel. - */ - private BrowserPanel() { + public BrowserPanel() { + super(FXML); + // To prevent triggering events for typing inside the loaded Web page. + getRoot().setOnKeyPressed(Event::consume); + + loadDefaultPage(); + registerAsAnEventHandler(this); } - @Override - public void setNode(Node node) { - //not applicable + private void loadPersonPage(ReadOnlyPerson person) { + loadPage(GOOGLE_SEARCH_URL_PREFIX + person.getName().fullName.replaceAll(" ", "+") + + GOOGLE_SEARCH_URL_SUFFIX); } - @Override - public String getFxmlPath() { - return null; //not applicable + public void loadPage(String url) { + Platform.runLater(() -> browser.getEngine().load(url)); } /** - * Factory method for creating a Browser Panel. - * This method should be called after the FX runtime is initialized and in FX application thread. - * @param placeholder The AnchorPane where the BrowserPanel must be inserted + * Loads a default HTML file with a background that matches the general theme. */ - public static BrowserPanel load(AnchorPane placeholder){ - logger.info("Initializing browser"); - BrowserPanel browserPanel = new BrowserPanel(); - browserPanel.browser = new WebView(); - placeholder.setOnKeyPressed(Event::consume); // To prevent triggering events for typing inside the loaded Web page. - FxViewUtil.applyAnchorBoundaryParameters(browserPanel.browser, 0.0, 0.0, 0.0, 0.0); - placeholder.getChildren().add(browserPanel.browser); - return browserPanel; - } - - public void loadPersonPage(ReadOnlyPerson person) { - loadPage("https://www.google.com.sg/#safe=off&q=" + person.getName().fullName.replaceAll(" ", "+")); - } - - public void loadPage(String url){ - browser.getEngine().load(url); + private void loadDefaultPage() { + URL defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE); + loadPage(defaultPage.toExternalForm()); } /** @@ -65,4 +65,9 @@ public void freeResources() { browser = null; } + @Subscribe + private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + loadPersonPage(event.getNewSelection().person); + } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 2e1409a3016c..5adc654471a2 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,114 +1,151 @@ package seedu.address.ui; -import com.google.common.eventbus.Subscribe; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.control.SplitPane; import javafx.scene.control.TextField; -import javafx.scene.layout.AnchorPane; -import javafx.stage.Stage; -import seedu.address.commons.events.ui.IncorrectCommandAttemptedEvent; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.*; -import seedu.address.commons.util.FxViewUtil; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.NewResultAvailableEvent; +import seedu.address.logic.ListElementPointer; +import seedu.address.logic.Logic; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; -import java.util.logging.Logger; +/** + * The UI component that is responsible for receiving user command inputs. + */ +public class CommandBox extends UiPart { -public class CommandBox extends UiPart { - private final Logger logger = LogsCenter.getLogger(CommandBox.class); + public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; - private AnchorPane placeHolderPane; - private AnchorPane commandPane; - private ResultDisplay resultDisplay; - String previousCommandTest; - - private Logic logic; + private final Logger logger = LogsCenter.getLogger(CommandBox.class); + private final Logic logic; + private ListElementPointer historySnapshot; @FXML private TextField commandTextField; - private CommandResult mostRecentResult; - - public static CommandBox load(Stage primaryStage, AnchorPane commandBoxPlaceholder, - ResultDisplay resultDisplay, Logic logic) { - CommandBox commandBox = UiPartLoader.loadUiPart(primaryStage, commandBoxPlaceholder, new CommandBox()); - commandBox.configure(resultDisplay, logic); - commandBox.addToPlaceholder(); - return commandBox; - } - public void configure(ResultDisplay resultDisplay, Logic logic) { - this.resultDisplay = resultDisplay; + public CommandBox(Logic logic) { + super(FXML); this.logic = logic; - registerAsAnEventHandler(this); + // calls #setStyleToDefault() whenever there is a change to the text of the command box. + commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + historySnapshot = logic.getHistorySnapshot(); } - private void addToPlaceholder() { - SplitPane.setResizableWithParent(placeHolderPane, false); - placeHolderPane.getChildren().add(commandTextField); - FxViewUtil.applyAnchorBoundaryParameters(commandPane, 0.0, 0.0, 0.0, 0.0); - FxViewUtil.applyAnchorBoundaryParameters(commandTextField, 0.0, 0.0, 0.0, 0.0); + /** + * Handles the key press event, {@code keyEvent}. + */ + @FXML + private void handleKeyPress(KeyEvent keyEvent) { + switch (keyEvent.getCode()) { + case UP: + // As up and down buttons will alter the position of the caret, + // consuming it causes the caret's position to remain unchanged + keyEvent.consume(); + + navigateToPreviousInput(); + break; + case DOWN: + keyEvent.consume(); + navigateToNextInput(); + break; + default: + // let JavaFx handle the keypress + } } - @Override - public void setNode(Node node) { - commandPane = (AnchorPane) node; - } + /** + * Updates the text field with the previous input in {@code historySnapshot}, + * if there exists a previous input in {@code historySnapshot} + */ + private void navigateToPreviousInput() { + assert historySnapshot != null; + if (!historySnapshot.hasPrevious()) { + return; + } - @Override - public String getFxmlPath() { - return FXML; + replaceText(historySnapshot.previous()); } - @Override - public void setPlaceholder(AnchorPane pane) { - this.placeHolderPane = pane; + /** + * Updates the text field with the next input in {@code historySnapshot}, + * if there exists a next input in {@code historySnapshot} + */ + private void navigateToNextInput() { + assert historySnapshot != null; + if (!historySnapshot.hasNext()) { + return; + } + + replaceText(historySnapshot.next()); } + /** + * Sets {@code CommandBox}'s text field with {@code text} and + * positions the caret to the end of the {@code text}. + */ + private void replaceText(String text) { + commandTextField.setText(text); + commandTextField.positionCaret(commandTextField.getText().length()); + } + /** + * Handles the Enter button pressed event. + */ @FXML private void handleCommandInputChanged() { - //Take a copy of the command text - previousCommandTest = commandTextField.getText(); - - /* We assume the command is correct. If it is incorrect, the command box will be changed accordingly - * in the event handling code {@link #handleIncorrectCommandAttempted} - */ - setStyleToIndicateCorrectCommand(); - mostRecentResult = logic.execute(previousCommandTest); - resultDisplay.postMessage(mostRecentResult.feedbackToUser); - logger.info("Result: " + mostRecentResult.feedbackToUser); + try { + CommandResult commandResult = logic.execute(commandTextField.getText()); + initHistory(); + historySnapshot.next(); + // process result of the command + commandTextField.setText(""); + logger.info("Result: " + commandResult.feedbackToUser); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser, false)); + + } catch (CommandException | ParseException e) { + initHistory(); + // handle command failure + setStyleToIndicateCommandFailure(); + logger.info("Invalid command: " + commandTextField.getText()); + raise(new NewResultAvailableEvent(e.getMessage(), true)); + } } - /** - * Sets the command box style to indicate a correct command. + * Initializes the history snapshot. */ - private void setStyleToIndicateCorrectCommand() { - commandTextField.getStyleClass().remove("error"); - commandTextField.setText(""); - } - - @Subscribe - private void handleIncorrectCommandAttempted(IncorrectCommandAttemptedEvent event){ - logger.info(LogsCenter.getEventHandlingLogMessage(event,"Invalid command: " + previousCommandTest)); - setStyleToIndicateIncorrectCommand(); - restoreCommandText(); + private void initHistory() { + historySnapshot = logic.getHistorySnapshot(); + // add an empty string to represent the most-recent end of historySnapshot, to be shown to + // the user if she tries to navigate past the most-recent end of the historySnapshot. + historySnapshot.add(""); } /** - * Restores the command box text to the previously entered command + * Sets the command box style to use the default style. */ - private void restoreCommandText() { - commandTextField.setText(previousCommandTest); + private void setStyleToDefault() { + commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); } /** - * Sets the command box style to indicate an error + * Sets the command box style to indicate a failed command. */ - private void setStyleToIndicateIncorrectCommand() { - commandTextField.getStyleClass().add("error"); + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = commandTextField.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); } } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 45b765ab6a0c..c6da79fd1538 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -1,62 +1,64 @@ package seedu.address.ui; -import javafx.scene.Node; +import java.util.logging.Logger; + +import javafx.fxml.FXML; import javafx.scene.Scene; -import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Region; import javafx.scene.web.WebView; import javafx.stage.Stage; -import seedu.address.commons.util.FxViewUtil; import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; +import seedu.address.commons.util.FxViewUtil; /** * Controller for a help page */ -public class HelpWindow extends UiPart { +public class HelpWindow extends UiPart { + + public static final String USERGUIDE_FILE_PATH = "/docs/UserGuide.html"; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String ICON = "/images/help_icon.png"; private static final String FXML = "HelpWindow.fxml"; private static final String TITLE = "Help"; - private static final String USERGUIDE_URL = - "https://github.com/se-edu/addressbook-level4/blob/master/docs/UserGuide.md"; - private AnchorPane mainPane; + @FXML + private WebView browser; - private Stage dialogStage; + private final Stage dialogStage; - public static HelpWindow load(Stage primaryStage) { - logger.fine("Showing help page about the application."); - HelpWindow helpWindow = UiPartLoader.loadUiPart(primaryStage, new HelpWindow()); - helpWindow.configure(); - return helpWindow; - } - - @Override - public void setNode(Node node) { - mainPane = (AnchorPane) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - private void configure(){ - Scene scene = new Scene(mainPane); + public HelpWindow() { + super(FXML); + Scene scene = new Scene(getRoot()); //Null passed as the parent stage to make it non-modal. dialogStage = createDialogStage(TITLE, null, scene); dialogStage.setMaximized(true); //TODO: set a more appropriate initial size - setIcon(dialogStage, ICON); + FxViewUtil.setStageIcon(dialogStage, ICON); - WebView browser = new WebView(); - browser.getEngine().load(USERGUIDE_URL); - FxViewUtil.applyAnchorBoundaryParameters(browser, 0.0, 0.0, 0.0, 0.0); - mainPane.getChildren().add(browser); + String userGuideUrl = getClass().getResource(USERGUIDE_FILE_PATH).toString(); + browser.getEngine().load(userGuideUrl); } + /** + * Shows the help window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ public void show() { + logger.fine("Showing help page about the application."); dialogStage.showAndWait(); } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 2c76aced3b04..af0777775b51 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,137 +1,155 @@ package seedu.address.ui; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.MenuItem; +import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.VBox; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; import seedu.address.commons.core.Config; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.ui.ExitAppRequestEvent; +import seedu.address.commons.events.ui.ShowHelpRequestEvent; +import seedu.address.commons.util.FxViewUtil; import seedu.address.logic.Logic; import seedu.address.model.UserPrefs; -import seedu.address.model.person.ReadOnlyPerson; /** * The Main Window. Provides the basic application layout containing * a menu bar and space where other JavaFX elements can be placed. */ -public class MainWindow extends UiPart { +public class MainWindow extends UiPart { private static final String ICON = "/images/address_book_32.png"; private static final String FXML = "MainWindow.fxml"; - public static final int MIN_HEIGHT = 600; - public static final int MIN_WIDTH = 450; + private static final int MIN_HEIGHT = 600; + private static final int MIN_WIDTH = 450; + + private final Logger logger = LogsCenter.getLogger(this.getClass()); + private Stage primaryStage; private Logic logic; // Independent Ui parts residing in this Ui container private BrowserPanel browserPanel; private PersonListPanel personListPanel; - private ResultDisplay resultDisplay; - private StatusBarFooter statusBarFooter; - private CommandBox commandBox; private Config config; - private UserPrefs userPrefs; - - // Handles to elements of this Ui container - private VBox rootLayout; - private Scene scene; - - private String addressBookName; + private UserPrefs prefs; @FXML - private AnchorPane browserPlaceholder; + private StackPane browserPlaceholder; @FXML - private AnchorPane commandBoxPlaceholder; + private StackPane commandBoxPlaceholder; @FXML private MenuItem helpMenuItem; @FXML - private AnchorPane personListPanelPlaceholder; + private StackPane searchBoxPlaceholder; @FXML - private AnchorPane resultDisplayPlaceholder; + private StackPane personListPanelPlaceholder; @FXML - private AnchorPane statusbarPlaceholder; - - - public MainWindow() { - super(); - } - - @Override - public void setNode(Node node) { - rootLayout = (VBox) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } + private StackPane resultDisplayPlaceholder; - public static MainWindow load(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { - - MainWindow mainWindow = UiPartLoader.loadUiPart(primaryStage, new MainWindow()); - mainWindow.configure(config.getAppTitle(), config.getAddressBookName(), config, prefs, logic); - return mainWindow; - } + @FXML + private StackPane statusbarPlaceholder; - private void configure(String appTitle, String addressBookName, Config config, UserPrefs prefs, - Logic logic) { + public MainWindow(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { + super(FXML); - //Set dependencies + // Set dependencies + this.primaryStage = primaryStage; this.logic = logic; - this.addressBookName = addressBookName; this.config = config; - this.userPrefs = prefs; + this.prefs = prefs; - //Configure the UI - setTitle(appTitle); + // Configure the UI + setTitle(config.getAppTitle()); setIcon(ICON); setWindowMinSize(); setWindowDefaultSize(prefs); - scene = new Scene(rootLayout); + Scene scene = new Scene(getRoot()); primaryStage.setScene(scene); setAccelerators(); + registerAsAnEventHandler(this); } - private void setAccelerators() { - helpMenuItem.setAccelerator(KeyCombination.valueOf("F1")); + public Stage getPrimaryStage() { + return primaryStage; } - void fillInnerParts() { - browserPanel = BrowserPanel.load(browserPlaceholder); - personListPanel = PersonListPanel.load(primaryStage, getPersonListPlaceholder(), logic.getFilteredPersonList()); - resultDisplay = ResultDisplay.load(primaryStage, getResultDisplayPlaceholder()); - statusBarFooter = StatusBarFooter.load(primaryStage, getStatusbarPlaceholder(), config.getAddressBookFilePath()); - commandBox = CommandBox.load(primaryStage, getCommandBoxPlaceholder(), resultDisplay, logic); + private void setAccelerators() { + setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); } - private AnchorPane getCommandBoxPlaceholder() { - return commandBoxPlaceholder; + /** + * Sets the accelerator of a MenuItem. + * @param keyCombination the KeyCombination value of the accelerator + */ + private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { + menuItem.setAccelerator(keyCombination); + + /* + * TODO: the code below can be removed once the bug reported here + * https://bugs.openjdk.java.net/browse/JDK-8131666 + * is fixed in later version of SDK. + * + * According to the bug report, TextInputControl (TextField, TextArea) will + * consume function-key events. Because CommandBox contains a TextField, and + * ResultDisplay contains a TextArea, thus some accelerators (e.g F1) will + * not work when the focus is in them because the key event is consumed by + * the TextInputControl(s). + * + * For now, we add following event filter to capture such key events and open + * help window purposely so to support accelerators even when focus is + * in CommandBox or ResultDisplay. + */ + getRoot().addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { + menuItem.getOnAction().handle(new ActionEvent()); + event.consume(); + } + }); } - private AnchorPane getStatusbarPlaceholder() { - return statusbarPlaceholder; - } + /** + * Fills up all the placeholders of this window. + */ + void fillInnerParts() { + browserPanel = new BrowserPanel(); + browserPlaceholder.getChildren().add(browserPanel.getRoot()); - private AnchorPane getResultDisplayPlaceholder() { - return resultDisplayPlaceholder; - } + personListPanel = new PersonListPanel(logic.getFilteredPersonList()); + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + + ResultDisplay resultDisplay = new ResultDisplay(); + resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + + StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath(), + logic.getFilteredPersonList().size()); + statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - public AnchorPane getPersonListPlaceholder() { - return personListPanelPlaceholder; + CommandBox commandBox = new CommandBox(logic); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + SearchBox searchBox = new SearchBox(logic); + searchBoxPlaceholder.getChildren().add(searchBox.getRoot()); } - public void hide() { + void hide() { primaryStage.hide(); } @@ -139,10 +157,18 @@ private void setTitle(String appTitle) { primaryStage.setTitle(appTitle); } + /** + * Sets the given image as the icon of the main window. + * @param iconSource e.g. {@code "/images/help_icon.png"} + */ + private void setIcon(String iconSource) { + FxViewUtil.setStageIcon(primaryStage, iconSource); + } + /** * Sets the default size based on user preferences. */ - protected void setWindowDefaultSize(UserPrefs prefs) { + private void setWindowDefaultSize(UserPrefs prefs) { primaryStage.setHeight(prefs.getGuiSettings().getWindowHeight()); primaryStage.setWidth(prefs.getGuiSettings().getWindowWidth()); if (prefs.getGuiSettings().getWindowCoordinates() != null) { @@ -159,18 +185,21 @@ private void setWindowMinSize() { /** * Returns the current size and the position of the main Window. */ - public GuiSettings getCurrentGuiSetting() { + GuiSettings getCurrentGuiSetting() { return new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), (int) primaryStage.getX(), (int) primaryStage.getY()); } + /** + * Opens the help window. + */ @FXML public void handleHelp() { - HelpWindow helpWindow = HelpWindow.load(primaryStage); + HelpWindow helpWindow = new HelpWindow(); helpWindow.show(); } - public void show() { + void show() { primaryStage.show(); } @@ -186,11 +215,13 @@ public PersonListPanel getPersonListPanel() { return this.personListPanel; } - public void loadPersonPage(ReadOnlyPerson person) { - browserPanel.loadPersonPage(person); + void releaseResources() { + browserPanel.freeResources(); } - public void releaseResources() { - browserPanel.freeResources(); + @Subscribe + private void handleShowHelpEvent(ShowHelpRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + handleHelp(); } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 259e9ad0d333..c95d2331352a 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,14 +1,35 @@ package seedu.address.ui; +import java.util.HashMap; +import java.util.Random; + +import javafx.beans.binding.Bindings; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; import seedu.address.model.person.ReadOnlyPerson; -public class PersonCard extends UiPart{ +/** + * An UI component that displays information of a {@code Person}. + */ +public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; + private static HashMap tagToColor = new HashMap(); + + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final ReadOnlyPerson person; + private Random random = new Random(System.currentTimeMillis()); @FXML private HBox cardPane; @@ -21,45 +42,92 @@ public class PersonCard extends UiPart{ @FXML private Label address; @FXML + private Label dob; + @FXML private Label email; @FXML - private Label tags; + private FlowPane tags; - private ReadOnlyPerson person; - private int displayedIndex; + public PersonCard(ReadOnlyPerson person, int displayedIndex) { + super(FXML); + this.person = person; + id.setText(displayedIndex + ". "); + initTags(person); + bindListeners(person); + } - public PersonCard(){ + /** + * Generate random colour with slight dark tint + * and return it as hexadecimal String + */ + private String generateRandomColor() { + // Factor to divide down the random color to better contrast white text + final double darkColorBase = 1.2; - } + final int red = (int) Math.round((random.nextInt(256)) / darkColorBase); + final int green = (int) Math.round((random.nextInt(256)) / darkColorBase); + final int blue = (int) Math.round((random.nextInt(256)) / darkColorBase); - public static PersonCard load(ReadOnlyPerson person, int displayedIndex){ - PersonCard card = new PersonCard(); - card.person = person; - card.displayedIndex = displayedIndex; - return UiPartLoader.loadUiPart(card); + // Convert RBG to Hex String + return String.format("#%02x%02x%02x", red, blue, green); } - @FXML - public void initialize() { - name.setText(person.getName().fullName); - id.setText(displayedIndex + ". "); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - tags.setText(person.tagsString()); + /** + * Store initialized tag into HashMap to remember the colour already given + * so that subsequent occurence will use the same colour + */ + private String getTagColor(String tagName) { + if (!tagToColor.containsKey(tagName)) { + tagToColor.put(tagName, generateRandomColor()); + } + return tagToColor.get(tagName); } - public HBox getLayout() { - return cardPane; + + /** + * Binds the individual UI elements to observe their respective {@code Person} properties + * so that they will be notified of any changes. + */ + private void bindListeners(ReadOnlyPerson person) { + name.textProperty().bind(Bindings.convert(person.nameProperty())); + phone.textProperty().bind(Bindings.convert(person.phoneProperty())); + address.textProperty().bind(Bindings.convert(person.addressProperty())); + dob.textProperty().bind(Bindings.convert(person.dobProperty())); + email.textProperty().bind(Bindings.convert(person.emailProperty())); + person.tagProperty().addListener((observable, oldValue, newValue) -> { + tags.getChildren().clear(); + initTags(person); + }); } - @Override - public void setNode(Node node) { - cardPane = (HBox)node; + /** + * Retrieve all tags from a person and initialize them + * with a unique tag colour + */ + private void initTags(ReadOnlyPerson person) { + person.getTags().forEach(tag -> { + Label uniqueTagLabel = new Label(tag.tagName); + uniqueTagLabel.setStyle("-fx-background-color: " + getTagColor(tag.tagName)); + tags.getChildren().add(uniqueTagLabel); + }); } + @Override - public String getFxmlPath() { - return FXML; + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonCard)) { + return false; + } + + // state check + PersonCard card = (PersonCard) other; + return id.getText().equals(card.id.getText()) + && person.equals(card.person); } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index 27d9381c47b5..d3a5fbae42bb 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -1,106 +1,86 @@ package seedu.address.ui; +import java.util.logging.Logger; + +import org.fxmisc.easybind.EasyBind; + +import com.google.common.eventbus.Subscribe; + import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import javafx.scene.control.SplitPane; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.VBox; -import javafx.stage.Stage; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.JumpToListRequestEvent; import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; import seedu.address.model.person.ReadOnlyPerson; -import seedu.address.commons.core.LogsCenter; - -import java.util.logging.Logger; /** * Panel containing the list of persons. */ -public class PersonListPanel extends UiPart { - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); +public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; - private VBox panel; - private AnchorPane placeHolderPane; + private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); @FXML - private ListView personListView; - - public PersonListPanel() { - super(); - } - - @Override - public void setNode(Node node) { - panel = (VBox) node; - } - - @Override - public String getFxmlPath() { - return FXML; - } - - @Override - public void setPlaceholder(AnchorPane pane) { - this.placeHolderPane = pane; - } - - public static PersonListPanel load(Stage primaryStage, AnchorPane personListPlaceholder, - ObservableList personList) { - PersonListPanel personListPanel = - UiPartLoader.loadUiPart(primaryStage, personListPlaceholder, new PersonListPanel()); - personListPanel.configure(personList); - return personListPanel; - } + private ListView personListView; - private void configure(ObservableList personList) { + public PersonListPanel(ObservableList personList) { + super(FXML); setConnections(personList); - addToPlaceholder(); + registerAsAnEventHandler(this); } private void setConnections(ObservableList personList) { - personListView.setItems(personList); + ObservableList mappedList = EasyBind.map( + personList, (person) -> new PersonCard(person, personList.indexOf(person) + 1)); + personListView.setItems(mappedList); personListView.setCellFactory(listView -> new PersonListViewCell()); setEventHandlerForSelectionChangeEvent(); } - private void addToPlaceholder() { - SplitPane.setResizableWithParent(placeHolderPane, false); - placeHolderPane.getChildren().add(panel); - } - private void setEventHandlerForSelectionChangeEvent() { - personListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); - raise(new PersonPanelSelectionChangedEvent(newValue)); - } - }); + personListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in person list panel changed to : '" + newValue + "'"); + raise(new PersonPanelSelectionChangedEvent(newValue)); + } + }); } - public void scrollTo(int index) { + /** + * Scrolls to the {@code PersonCard} at the {@code index} and selects it. + */ + private void scrollTo(int index) { Platform.runLater(() -> { personListView.scrollTo(index); personListView.getSelectionModel().clearAndSelect(index); }); } - class PersonListViewCell extends ListCell { + @Subscribe + private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + scrollTo(event.targetIndex); + } - public PersonListViewCell() { - } + /** + * Custom {@code ListCell} that displays the graphics of a {@code PersonCard}. + */ + class PersonListViewCell extends ListCell { @Override - protected void updateItem(ReadOnlyPerson person, boolean empty) { + protected void updateItem(PersonCard person, boolean empty) { super.updateItem(person, empty); if (empty || person == null) { setGraphic(null); setText(null); } else { - setGraphic(PersonCard.load(person, getIndex() + 1).getLayout()); + setGraphic(person.getRoot()); } } } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 37284ee6c696..e14df5f7b6d5 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -1,65 +1,65 @@ package seedu.address.ui; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.scene.Node; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; import javafx.scene.control.TextArea; -import javafx.scene.layout.AnchorPane; -import javafx.stage.Stage; -import seedu.address.commons.util.FxViewUtil; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.NewResultAvailableEvent; /** * A ui for the status bar that is displayed at the header of the application. */ -public class ResultDisplay extends UiPart { - public static final String RESULT_DISPLAY_ID = "resultDisplay"; - private static final String STATUS_BAR_STYLE_SHEET = "result-display"; - private TextArea resultDisplayArea; - private final StringProperty displayed = new SimpleStringProperty(""); +public class ResultDisplay extends UiPart { + private static final String ERROR_STYLE_CLASS = "error"; + + private static final Logger logger = LogsCenter.getLogger(ResultDisplay.class); private static final String FXML = "ResultDisplay.fxml"; - private AnchorPane placeHolder; + private final StringProperty displayed = new SimpleStringProperty(""); - private AnchorPane mainPane; + @FXML + private TextArea resultDisplay; - public static ResultDisplay load(Stage primaryStage, AnchorPane placeHolder) { - ResultDisplay statusBar = UiPartLoader.loadUiPart(primaryStage, placeHolder, new ResultDisplay()); - statusBar.configure(); - return statusBar; + public ResultDisplay() { + super(FXML); + resultDisplay.textProperty().bind(displayed); + registerAsAnEventHandler(this); } - public void configure() { - resultDisplayArea = new TextArea(); - resultDisplayArea.setEditable(false); - resultDisplayArea.setId(RESULT_DISPLAY_ID); - resultDisplayArea.getStyleClass().removeAll(); - resultDisplayArea.getStyleClass().add(STATUS_BAR_STYLE_SHEET); - resultDisplayArea.setText(""); - resultDisplayArea.textProperty().bind(displayed); - FxViewUtil.applyAnchorBoundaryParameters(resultDisplayArea, 0.0, 0.0, 0.0, 0.0); - mainPane.getChildren().add(resultDisplayArea); - FxViewUtil.applyAnchorBoundaryParameters(mainPane, 0.0, 0.0, 0.0, 0.0); - placeHolder.getChildren().add(mainPane); + private void setStyleToDefault() { + resultDisplay.getStyleClass().remove(ERROR_STYLE_CLASS); } - @Override - public void setNode(Node node) { - mainPane = (AnchorPane) node; - } + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = resultDisplay.getStyleClass(); - @Override - public void setPlaceholder(AnchorPane placeholder) { - this.placeHolder = placeholder; - } + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } - @Override - public String getFxmlPath() { - return FXML; + styleClass.add(ERROR_STYLE_CLASS); } - public void postMessage(String message) { - displayed.setValue(message); + @Subscribe + private void handleNewResultAvailableEvent(NewResultAvailableEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + Platform.runLater(() -> displayed.setValue(event.message)); + + if (event.isError) { + setStyleToIndicateCommandFailure(); + } else { + setStyleToDefault(); + } } + } diff --git a/src/main/java/seedu/address/ui/SearchBox.java b/src/main/java/seedu/address/ui/SearchBox.java new file mode 100644 index 000000000000..24c92d84d05a --- /dev/null +++ b/src/main/java/seedu/address/ui/SearchBox.java @@ -0,0 +1,85 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.NewResultAvailableEvent; +import seedu.address.logic.Logic; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * * The UI component that is responsible for real-time partial searching of contact name + */ +public class SearchBox extends UiPart { + + private static final String ERROR_STYLE_CLASS = "error"; + + private static final Logger logger = LogsCenter.getLogger(SearchBox.class); + private static final String FXML = "SearchBox.fxml"; + + @FXML + private TextField searchTextField; + + public SearchBox (Logic logic) { + super(FXML); + // track all changes in the searchTextField and call for partial search real-time. + searchTextField.textProperty().addListener((observable, oldValue, newValue) -> { + // show list of all contact when this textfield is empty + if (newValue.equals("")) { + try { + CommandResult commandResult = logic.execute("list"); + logger.info("Result: " + commandResult.feedbackToUser); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser, false)); + + } catch (CommandException | ParseException e) { + // handle command failure + setStyleToIndicateCommandFailure(); + logger.info("Invalid command: list"); + raise(new NewResultAvailableEvent(e.getMessage(), true)); + } + } else { + try { + CommandResult commandResult = logic.execute("pfind " + newValue); + logger.info("Result: " + commandResult.feedbackToUser); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser, false)); + + } catch (CommandException | ParseException e) { + // handle command failure + setStyleToIndicateCommandFailure(); + logger.info("Invalid command: pfind " + newValue); + raise(new NewResultAvailableEvent(e.getMessage(), true)); + } + } + + }); + } + + /** + * Sets the search box style to use the default style. + */ + private void setStyleToDefault() { + searchTextField.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the search box style to indicate a failed command. + */ + + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = searchTextField.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f74f66be6fc9..aff6c53320ed 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,98 +1,89 @@ package seedu.address.ui; +import java.time.Clock; +import java.util.Date; +import java.util.logging.Logger; + +import org.controlsfx.control.StatusBar; + import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.Node; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.GridPane; -import javafx.stage.Stage; -import org.controlsfx.control.StatusBar; +import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.util.FxViewUtil; - -import java.util.Date; -import java.util.logging.Logger; /** * A ui for the status bar that is displayed at the footer of the application. */ -public class StatusBarFooter extends UiPart { - private static final Logger logger = LogsCenter.getLogger(StatusBarFooter.class); - private StatusBar syncStatus; - private StatusBar saveLocationStatus; +public class StatusBarFooter extends UiPart { - private GridPane mainPane; + public static final String SYNC_STATUS_INITIAL = " Not updated yet in this session"; + public static final String SYNC_STATUS_UPDATED = " Last Updated: %s"; - @FXML - private AnchorPane saveLocStatusBarPane; + /** + * Used to generate time stamps. + * + * TODO: change clock to an instance variable. + * We leave it as a static variable because manual dependency injection + * will require passing down the clock reference all the way from MainApp, + * but it should be easier once we have factories/DI frameworks. + */ + private static Clock clock = Clock.systemDefaultZone(); - @FXML - private AnchorPane syncStatusBarPane; - - private AnchorPane placeHolder; + private static final Logger logger = LogsCenter.getLogger(StatusBarFooter.class); private static final String FXML = "StatusBarFooter.fxml"; - public static StatusBarFooter load(Stage stage, AnchorPane placeHolder, String saveLocation) { - StatusBarFooter statusBarFooter = UiPartLoader.loadUiPart(stage, placeHolder, new StatusBarFooter()); - statusBarFooter.configure(saveLocation); - return statusBarFooter; - } + @FXML + private StatusBar syncStatus; + @FXML + private StatusBar saveLocationStatus; + @FXML + private StatusBar showNumberOfContacts; - public void configure(String saveLocation) { - addMainPane(); - addSyncStatus(); - setSyncStatus("Not updated yet in this session"); - addSaveLocation(); + + public StatusBarFooter(String saveLocation, int numberOfContacts) { + super(FXML); + setSyncStatus(SYNC_STATUS_INITIAL); setSaveLocation("./" + saveLocation); + setNumberOfContacts(numberOfContacts); registerAsAnEventHandler(this); } - private void addMainPane() { - FxViewUtil.applyAnchorBoundaryParameters(mainPane, 0.0, 0.0, 0.0, 0.0); - placeHolder.getChildren().add(mainPane); + /** + * Sets the clock used to determine the current time. + */ + public static void setClock(Clock clock) { + StatusBarFooter.clock = clock; } - private void setSaveLocation(String location) { - this.saveLocationStatus.setText(location); + /** + * Returns the clock currently in use. + */ + public static Clock getClock() { + return clock; } - private void addSaveLocation() { - this.saveLocationStatus = new StatusBar(); - FxViewUtil.applyAnchorBoundaryParameters(saveLocationStatus, 0.0, 0.0, 0.0, 0.0); - saveLocStatusBarPane.getChildren().add(saveLocationStatus); + private void setSaveLocation(String location) { + Platform.runLater(() -> this.saveLocationStatus.setText(location)); } private void setSyncStatus(String status) { - this.syncStatus.setText(status); - } - - private void addSyncStatus() { - this.syncStatus = new StatusBar(); - FxViewUtil.applyAnchorBoundaryParameters(syncStatus, 0.0, 0.0, 0.0, 0.0); - syncStatusBarPane.getChildren().add(syncStatus); - } - - @Override - public void setNode(Node node) { - mainPane = (GridPane) node; - } - - @Override - public void setPlaceholder(AnchorPane placeholder) { - this.placeHolder = placeholder; - } - - @Override - public String getFxmlPath() { - return FXML; + Platform.runLater(() -> this.syncStatus.setText(status)); } @Subscribe public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { - String lastUpdated = (new Date()).toString(); + long now = clock.millis(); + String lastUpdated = new Date(now).toString(); logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); - setSyncStatus("Last Updated: " + lastUpdated); + setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); + setNumberOfContacts(abce.data.getPersonList().size()); + } + + public void setNumberOfContacts(int numberOfContacts) { + showNumberOfContacts.setText(" " + numberOfContacts + "person(s) total"); } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 4a4dba3a2f6e..efce347afdfb 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -1,6 +1,9 @@ package seedu.address.ui; +import java.util.logging.Logger; + import com.google.common.eventbus.Subscribe; + import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; @@ -11,19 +14,21 @@ import seedu.address.commons.core.Config; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.storage.DataSavingExceptionEvent; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.commons.events.ui.ShowHelpRequestEvent; import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.model.UserPrefs; -import java.util.logging.Logger; - /** * The manager of the UI component. */ public class UiManager extends ComponentManager implements Ui { + + public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; + + public static final String FILE_OPS_ERROR_DIALOG_STAGE_TITLE = "File Op Error"; + public static final String FILE_OPS_ERROR_DIALOG_HEADER_MESSAGE = "Could not save data"; + public static final String FILE_OPS_ERROR_DIALOG_CONTENT_MESSAGE = "Could not save data to file"; + private static final Logger logger = LogsCenter.getLogger(UiManager.class); private static final String ICON_APPLICATION = "/images/address_book_32.png"; @@ -48,7 +53,7 @@ public void start(Stage primaryStage) { primaryStage.getIcons().add(getImage(ICON_APPLICATION)); try { - mainWindow = MainWindow.load(primaryStage, config, prefs, logic); + mainWindow = new MainWindow(primaryStage, config, prefs, logic); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); @@ -67,7 +72,7 @@ public void stop() { private void showFileOperationAlertAndWait(String description, String details, Throwable cause) { final String content = details + ":\n" + cause.toString(); - showAlertDialogAndWait(AlertType.ERROR, "File Op Error", description, content); + showAlertDialogAndWait(AlertType.ERROR, FILE_OPS_ERROR_DIALOG_STAGE_TITLE, description, content); } private Image getImage(String imagePath) { @@ -78,6 +83,10 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); } + /** + * Shows an alert dialog on {@code owner} with the given parameters. + * This method only returns after the user has closed the alert dialog. + */ private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); @@ -86,10 +95,14 @@ private static void showAlertDialogAndWait(Stage owner, AlertType type, String t alert.setTitle(title); alert.setHeaderText(headerText); alert.setContentText(contentText); - + alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); alert.showAndWait(); } + /** + * Shows an error alert dialog with {@code title} and error message, {@code e}, + * and exits the application after the user has closed the alert dialog. + */ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e)); showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString()); @@ -97,30 +110,12 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { System.exit(1); } - //==================== Event Handling Code ================================================================= + //==================== Event Handling Code =============================================================== @Subscribe private void handleDataSavingExceptionEvent(DataSavingExceptionEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - showFileOperationAlertAndWait("Could not save data", "Could not save data to file", event.exception); - } - - @Subscribe - private void handleShowHelpEvent(ShowHelpRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - mainWindow.handleHelp(); - } - - @Subscribe - private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - mainWindow.getPersonListPanel().scrollTo(event.targetIndex); + showFileOperationAlertAndWait(FILE_OPS_ERROR_DIALOG_HEADER_MESSAGE, FILE_OPS_ERROR_DIALOG_CONTENT_MESSAGE, + event.exception); } - - @Subscribe - private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event){ - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - mainWindow.loadPersonPage(event.getNewSelection()); - } - } diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/address/ui/UiPart.java index 0a4ceb33e9b7..4697a46d2ca8 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/address/ui/UiPart.java @@ -1,34 +1,64 @@ package seedu.address.ui; -import javafx.scene.Node; +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.net.URL; + +import javafx.fxml.FXMLLoader; import javafx.scene.Scene; -import javafx.scene.layout.AnchorPane; import javafx.stage.Modality; import javafx.stage.Stage; +import seedu.address.MainApp; import seedu.address.commons.core.EventsCenter; import seedu.address.commons.events.BaseEvent; -import seedu.address.commons.util.AppUtil; /** - * Base class for UI parts. - * A 'UI part' represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. + * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. + * It contains a scene graph with a root node of type {@code T}. */ -public abstract class UiPart { +public abstract class UiPart { + + /** Resource folder where FXML files are stored. */ + public static final String FXML_FILE_FOLDER = "/view/"; + + private FXMLLoader fxmlLoader; /** - * The primary stage for the UI Part. + * Constructs a UiPart with the specified FXML file URL. + * The FXML file must not specify the {@code fx:controller} attribute. */ - Stage primaryStage; + public UiPart(URL fxmlFileUrl) { + requireNonNull(fxmlFileUrl); + fxmlLoader = new FXMLLoader(fxmlFileUrl); + fxmlLoader.setController(this); + try { + fxmlLoader.load(); + } catch (IOException e) { + throw new AssertionError(e); + } + } - public UiPart(){ + /** + * Constructs a UiPart using the specified FXML file within {@link #FXML_FILE_FOLDER}. + * @see #UiPart(URL) + */ + public UiPart(String fxmlFileName) { + this(fxmlFileName != null ? MainApp.class.getResource(FXML_FILE_FOLDER + fxmlFileName) : null); + } + /** + * Returns the root object of the scene graph of this UiPart. + */ + public T getRoot() { + return fxmlLoader.getRoot(); } /** * Raises the event via {@link EventsCenter#post(BaseEvent)} * @param event */ - protected void raise(BaseEvent event){ + protected void raise(BaseEvent event) { EventsCenter.getInstance().post(event); } @@ -40,23 +70,6 @@ protected void registerAsAnEventHandler(Object handler) { EventsCenter.getInstance().registerHandler(handler); } - /** - * Override this method to receive the main Node generated while loading the view from the .fxml file. - * @param node - */ - public abstract void setNode(Node node); - - /** - * Override this method to return the name of the fxml file. e.g. {@code "MainWindow.fxml"} - * @return - */ - public abstract String getFxmlPath(); - - public void setStage(Stage primaryStage) { - this.primaryStage = primaryStage; - } - - /** * Creates a modal dialog. * @param title Title of the dialog. @@ -73,32 +86,4 @@ protected Stage createDialogStage(String title, Stage parentStage, Scene scene) return dialogStage; } - /** - * Sets the given image as the icon for the primary stage of this UI Part. - * @param iconSource e.g. {@code "/images/help_icon.png"} - */ - protected void setIcon(String iconSource) { - primaryStage.getIcons().add(AppUtil.getImage(iconSource)); - } - - /** - * Sets the given image as the icon for the given stage. - * @param stage - * @param iconSource e.g. {@code "/images/help_icon.png"} - */ - protected void setIcon(Stage stage, String iconSource) { - stage.getIcons().add(AppUtil.getImage(iconSource)); - } - - /** - * Sets the placeholder for UI parts that reside inside another UI part. - * @param placeholder - */ - public void setPlaceholder(AnchorPane placeholder) { - //Do nothing by default. - } - - public Stage getPrimaryStage() { - return primaryStage; - } } diff --git a/src/main/java/seedu/address/ui/UiPartLoader.java b/src/main/java/seedu/address/ui/UiPartLoader.java deleted file mode 100644 index f880685a5b15..000000000000 --- a/src/main/java/seedu/address/ui/UiPartLoader.java +++ /dev/null @@ -1,63 +0,0 @@ -package seedu.address.ui; - -import javafx.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.layout.AnchorPane; -import javafx.stage.Stage; -import seedu.address.MainApp; - -/** - * A utility class to load UiParts from FXML files. - */ -public class UiPartLoader { - private final static String FXML_FILE_FOLDER = "/view/"; - - public static T loadUiPart(Stage primaryStage, T controllerSeed) { - return loadUiPart(primaryStage, null, controllerSeed); - } - - /** - * Returns the ui class for a specific UI Part. - * - * @param primaryStage The primary stage for the view. - * @param placeholder The placeholder where the loaded Ui Part is added. - * @param sampleUiPart The sample of the expected UiPart class. - * @param The type of the UiPart - */ - public static T loadUiPart(Stage primaryStage, AnchorPane placeholder, T sampleUiPart) { - FXMLLoader loader = new FXMLLoader(); - loader.setLocation(MainApp.class.getResource(FXML_FILE_FOLDER + sampleUiPart.getFxmlPath())); - Node mainNode = loadLoader(loader, sampleUiPart.getFxmlPath()); - UiPart controller = loader.getController(); - controller.setStage(primaryStage); - controller.setPlaceholder(placeholder); - controller.setNode(mainNode); - return (T)controller; - } - - /** - * Returns the ui class for a specific UI Part. - * - * @param seedUiPart The UiPart object to be used as the ui. - * @param The type of the UiPart - */ - - public static T loadUiPart(T seedUiPart) { - FXMLLoader loader = new FXMLLoader(); - loader.setLocation(MainApp.class.getResource(FXML_FILE_FOLDER + seedUiPart.getFxmlPath())); - loader.setController(seedUiPart); - loadLoader(loader, seedUiPart.getFxmlPath()); - return seedUiPart; - } - - - private static Node loadLoader(FXMLLoader loader, String fxmlFileName) { - try { - return loader.load(); - } catch (Exception e) { - String errorMessage = "FXML Load Error for " + fxmlFileName; - throw new RuntimeException(errorMessage, e); - } - } - -} diff --git a/src/main/resources/view/BrowserPanel.fxml b/src/main/resources/view/BrowserPanel.fxml new file mode 100644 index 000000000000..31670827e3da --- /dev/null +++ b/src/main/resources/view/BrowserPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 575de420b994..16553ce8eadb 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,9 @@ - - - - + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 8043b344253a..d2306c5c1a70 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,5 +1,6 @@ .background { -fx-background-color: derive(#1d1d1d, 20%); + background-color: #383838; /* Used in the default.html file */ } .label { @@ -76,8 +77,8 @@ } .split-pane:horizontal .split-pane-divider { - -fx-border-color: transparent #1d1d1d transparent #1d1d1d; - -fx-background-color: transparent, derive(#1d1d1d, 10%); + -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { @@ -86,23 +87,47 @@ -fx-background-color: derive(#1d1d1d, 20%); } +.list-view { + -fx-background-insets: 0; + -fx-padding: 0; +} + .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; } +.list-cell:filled:even { + -fx-background-color: #3c3e3f; +} + +.list-cell:filled:odd { + -fx-background-color: #515658; +} + +.list-cell:filled:selected { + -fx-background-color: #424d5f; +} + +.list-cell:filled:selected #cardPane { + -fx-border-color: #3e7b91; + -fx-border-width: 1; +} + .list-cell .label { - -fx-text-fill: #010504; + -fx-text-fill: white; } .cell_big_label { + -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; -fx-text-fill: #010504; } .cell_small_label { - -fx-font-size: 11px; + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; -fx-text-fill: #010504; } @@ -110,7 +135,7 @@ -fx-background-color: derive(#1d1d1d, 20%); } -.anchor-pane-with-border { +.pane-with-border { -fx-background-color: derive(#1d1d1d, 20%); -fx-border-color: derive(#1d1d1d, 10%); -fx-border-top-width: 1px; @@ -122,7 +147,10 @@ } .result-display { - -fx-background-color: #ffffff; + -fx-background-color: transparent; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: white; } .result-display .label { @@ -130,6 +158,7 @@ } .status-bar .label { + -fx-font-family: "Segoe UI Light"; -fx-text-fill: white; } @@ -250,6 +279,10 @@ -fx-text-fill: white; } +.scroll-bar { + -fx-background-color: derive(#1d1d1d, 20%); +} + .scroll-bar .thumb { -fx-background-color: derive(#1d1d1d, 50%); -fx-background-insets: 3; @@ -274,8 +307,7 @@ #cardPane { -fx-background-color: transparent; - -fx-border-color: #d6d6d6; - -fx-border-width: 1 1 1 1; + -fx-border-width: 0; } #commandTypeLabel { @@ -283,6 +315,37 @@ -fx-text-fill: #F70D1A; } +#commandTextField, #searchTextField { + -fx-background-color: transparent #383838 transparent #383838; + -fx-background-insets: 0; + -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-insets: 0; + -fx-border-width: 1; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: white; +} + + #filterField, #personListPanel, #personWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); -} \ No newline at end of file +} + +#resultDisplay .content { + -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-radius: 0; +} + +#tags { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#tags .label { + -fx-text-fill: white; + -fx-background-color: #3e7b91; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} diff --git a/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml b/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml deleted file mode 100644 index bc761118235a..000000000000 --- a/src/main/resources/view/DefaultBrowserPlaceHolderScreen.fxml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index 606c927d9a42..bfe82a85964d 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -1,8 +1,12 @@ .error { - -fx-background-color: red; + -fx-text-fill: #d06651 !important; /* The error class should always override the default text-fill style */ } +.list-cell:empty { + /* Empty cells will not have alternating colours */ + -fx-background: #383838; +} .tag-selector { -fx-border-width: 1; @@ -13,4 +17,4 @@ .tooltip-text { -fx-text-fill: white; -} \ No newline at end of file +} diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index c4cbd84cac28..b4ffa2eabdc9 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,8 +1,8 @@ - - - + + - - + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 2f9235c621d8..e54fe3810eb3 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,56 +1,56 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 13d4b149651b..ec28d55690df 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -1,42 +1,37 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 000c4c999b65..8836d323cc5d 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,14 +1,8 @@ - - - - - - - - - - + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index cc650d739e22..58d5ad3dc56c 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -1,11 +1,9 @@ - - - - - - - - + + + + +