├── _config.yml ├── doc ├── Diagrams.pptx ├── images │ ├── Ui.png │ ├── mainClassDiagram.png │ ├── DependencyInjection.png │ ├── LogicStroageFileDIP.png │ ├── PrintableInterface.png │ ├── ReadOnlyPersonUsage.png │ └── DependencyInjectionWithoutDIP.png ├── UserGuide.md ├── DeveloperGuide.md └── LearningOutcomes.md ├── .travis.yml ├── test ├── data │ └── StorageFileTest │ │ ├── InvalidData.txt │ │ └── ValidData.txt └── java │ └── seedu │ └── addressbook │ ├── util │ └── TestUtil.java │ ├── common │ └── UtilsTest.java │ ├── storage │ └── StorageFileTest.java │ ├── parser │ └── ParserTest.java │ └── logic │ └── LogicTest.java ├── src └── seedu │ └── addressbook │ ├── ui │ ├── Stoppable.java │ ├── DarkTheme.css │ ├── mainwindow.fxml │ ├── Gui.java │ ├── Formatter.java │ └── MainWindow.java │ ├── data │ ├── exception │ │ ├── DuplicateDataException.java │ │ └── IllegalValueException.java │ ├── tag │ │ ├── Tag.java │ │ └── UniqueTagList.java │ ├── person │ │ ├── Address.java │ │ ├── Phone.java │ │ ├── Email.java │ │ ├── Name.java │ │ ├── Person.java │ │ ├── ReadOnlyPerson.java │ │ └── UniquePersonList.java │ └── AddressBook.java │ ├── commands │ ├── IncorrectCommand.java │ ├── ExitCommand.java │ ├── ClearCommand.java │ ├── ListCommand.java │ ├── HelpCommand.java │ ├── CommandResult.java │ ├── ViewAllCommand.java │ ├── ViewCommand.java │ ├── DeleteCommand.java │ ├── FindCommand.java │ ├── Command.java │ └── AddCommand.java │ ├── common │ ├── Messages.java │ └── Utils.java │ ├── Main.java │ ├── storage │ ├── jaxb │ │ ├── AdaptedTag.java │ │ ├── AdaptedAddressBook.java │ │ └── AdaptedPerson.java │ └── StorageFile.java │ ├── logic │ └── Logic.java │ └── parser │ └── Parser.java ├── .gitignore ├── LICENSE └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /doc/Diagrams.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/Diagrams.pptx -------------------------------------------------------------------------------- /doc/images/Ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/Ui.png -------------------------------------------------------------------------------- /doc/images/mainClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/mainClassDiagram.png -------------------------------------------------------------------------------- /doc/images/DependencyInjection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/DependencyInjection.png -------------------------------------------------------------------------------- /doc/images/LogicStroageFileDIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/LogicStroageFileDIP.png -------------------------------------------------------------------------------- /doc/images/PrintableInterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/PrintableInterface.png -------------------------------------------------------------------------------- /doc/images/ReadOnlyPersonUsage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/ReadOnlyPersonUsage.png -------------------------------------------------------------------------------- /doc/images/DependencyInjectionWithoutDIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kokonguyen191/addressbook-level3/HEAD/doc/images/DependencyInjectionWithoutDIP.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | matrix: 3 | include: 4 | - jdk: oraclejdk8 5 | 6 | 7 | addons: 8 | apt: 9 | packages: 10 | - oracle-java8-installer 11 | -------------------------------------------------------------------------------- /test/data/StorageFileTest/InvalidData.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | data 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/Stoppable.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.ui; 2 | 3 | /** 4 | * An App that can be stopped by calling the stop() method. 5 | */ 6 | public interface Stoppable { 7 | public void stop() throws Exception; 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated class files 2 | *.class 3 | 4 | # Default data file 5 | addressbook.txt 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # Idea files 13 | .idea/ 14 | *.iml 15 | out/ 16 | test/data/ 17 | /bin/ 18 | /data/ 19 | publish.sh 20 | 21 | # Gradle build files 22 | .gradle/ 23 | build/ 24 | 25 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/exception/DuplicateDataException.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.exception; 2 | 3 | /** 4 | * Signals an error caused by duplicate data where there should be none. 5 | */ 6 | public abstract class DuplicateDataException extends IllegalValueException { 7 | public DuplicateDataException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/exception/IllegalValueException.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.exception; 2 | 3 | /** 4 | * Signals that some given data does not fulfill some constraints. 5 | */ 6 | public class IllegalValueException extends Exception { 7 | /** 8 | * @param message should contain relevant information on the failed constraint(s) 9 | */ 10 | public IllegalValueException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/DarkTheme.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .text-field { 4 | -fx-font-size: 12pt; 5 | -fx-font-family: "Consolas"; 6 | -fx-font-weight: bold; 7 | -fx-text-fill: yellow; 8 | -fx-control-inner-background: derive(#1d1d1d,20%); 9 | } 10 | 11 | .text-area { 12 | -fx-background-color: black; 13 | -fx-control-inner-background: black; 14 | -fx-font-family: "Segoe UI Semibold"; 15 | -fx-font-size: 10pt; 16 | -fx-padding: 5 5 5 5; 17 | } 18 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/IncorrectCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | 4 | /** 5 | * Represents an incorrect command. Upon execution, produces some feedback to the user. 6 | */ 7 | public class IncorrectCommand extends Command{ 8 | 9 | public final String feedbackToUser; 10 | 11 | public IncorrectCommand(String feedbackToUser){ 12 | this.feedbackToUser = feedbackToUser; 13 | } 14 | 15 | @Override 16 | public CommandResult execute() { 17 | return new CommandResult(feedbackToUser); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/ExitCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | /** 4 | * Terminates the program. 5 | */ 6 | public class ExitCommand extends Command { 7 | 8 | public static final String COMMAND_WORD = "exit"; 9 | 10 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Exits the program.\n\t" 11 | + "Example: " + COMMAND_WORD; 12 | public static final String MESSAGE_EXIT_ACKNOWEDGEMENT = "Exiting Address Book as requested ..."; 13 | 14 | @Override 15 | public CommandResult execute() { 16 | return new CommandResult(MESSAGE_EXIT_ACKNOWEDGEMENT); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/ClearCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | /** 4 | * Clears the address book. 5 | */ 6 | public class ClearCommand extends Command { 7 | 8 | public static final String COMMAND_WORD = "clear"; 9 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Clears address book permanently.\n\t" 10 | + "Example: " + COMMAND_WORD; 11 | 12 | public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; 13 | 14 | @Override 15 | public CommandResult execute() { 16 | addressBook.clear(); 17 | return new CommandResult(MESSAGE_SUCCESS); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/mainwindow.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/data/StorageFileTest/ValidData.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | John Doe 5 | 98765432 6 | johnd@gmail.com 7 |
John street, block 123, #01-01
8 |
9 | 10 | Betsy Crowe 11 | 1234567 12 | betsycrowe@gmail.com 13 |
Newgate Prison
14 | friend 15 | criminal 16 |
17 | friend 18 | criminal 19 |
20 | -------------------------------------------------------------------------------- /test/java/seedu/addressbook/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.util; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import java.io.IOException; 5 | import java.nio.charset.Charset; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.List; 9 | 10 | public class TestUtil { 11 | /** 12 | * Asserts whether the text in the two given files are the same. Ignores any 13 | * differences in line endings 14 | */ 15 | public static void assertTextFilesEqual(Path path1, Path path2) throws IOException { 16 | List list1 = Files.readAllLines(path1, Charset.defaultCharset()); 17 | List list2 = Files.readAllLines(path2, Charset.defaultCharset()); 18 | assertEquals(String.join("\n", list1), String.join("\n", list2)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/ListCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.data.person.ReadOnlyPerson; 4 | 5 | import java.util.List; 6 | 7 | 8 | /** 9 | * Lists all persons in the address book to the user. 10 | */ 11 | public class ListCommand extends Command { 12 | 13 | public static final String COMMAND_WORD = "list"; 14 | 15 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" 16 | + "Displays all persons in the address book as a list with index numbers.\n\t" 17 | + "Example: " + COMMAND_WORD; 18 | 19 | 20 | @Override 21 | public CommandResult execute() { 22 | List allPersons = addressBook.getAllPersons().immutableListView(); 23 | return new CommandResult(getMessageForPersonListShownSummary(allPersons), allPersons); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/seedu/addressbook/common/Messages.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.common; 2 | 3 | /** 4 | * Container for user visible messages. 5 | */ 6 | public class Messages { 7 | 8 | public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; 9 | public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; 10 | public static final String MESSAGE_PERSON_NOT_IN_ADDRESSBOOK = "Person could not be found in address book"; 11 | public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; 12 | public static final String MESSAGE_PROGRAM_LAUNCH_ARGS_USAGE = "Launch command format: " + 13 | "java seedu.addressbook.Main [STORAGE_FILE_PATH]"; 14 | public static final String MESSAGE_WELCOME = "Welcome to your Address Book!"; 15 | public static final String MESSAGE_USING_STORAGE_FILE = "Using storage file : %1$s"; 16 | } 17 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/HelpCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | 4 | /** 5 | * Shows help instructions. 6 | */ 7 | public class HelpCommand extends Command { 8 | 9 | public static final String COMMAND_WORD = "help"; 10 | 11 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" +"Shows program usage instructions.\n\t" 12 | + "Example: " + COMMAND_WORD; 13 | 14 | public static final String MESSAGE_ALL_USAGES = AddCommand.MESSAGE_USAGE 15 | + "\n" + DeleteCommand.MESSAGE_USAGE 16 | + "\n" + ClearCommand.MESSAGE_USAGE 17 | + "\n" + FindCommand.MESSAGE_USAGE 18 | + "\n" + ListCommand.MESSAGE_USAGE 19 | + "\n" + ViewCommand.MESSAGE_USAGE 20 | + "\n" + ViewAllCommand.MESSAGE_USAGE 21 | + "\n" + HelpCommand.MESSAGE_USAGE 22 | + "\n" + ExitCommand.MESSAGE_USAGE; 23 | 24 | @Override 25 | public CommandResult execute() { 26 | return new CommandResult(MESSAGE_ALL_USAGES); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/seedu/addressbook/Main.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook; 2 | 3 | import javafx.application.Application; 4 | import javafx.application.Platform; 5 | 6 | import javafx.stage.Stage; 7 | import seedu.addressbook.logic.Logic; 8 | import seedu.addressbook.ui.Gui; 9 | import seedu.addressbook.ui.Stoppable; 10 | 11 | /** 12 | * Main entry point to the application. 13 | */ 14 | public class Main extends Application implements Stoppable{ 15 | 16 | /** Version info of the program. */ 17 | public static final String VERSION = "AddressBook Level 3 - Version 1.0"; 18 | 19 | private Gui gui; 20 | 21 | @Override 22 | public void start(Stage primaryStage) throws Exception{ 23 | gui = new Gui(new Logic(), VERSION); 24 | gui.start(primaryStage, this); 25 | } 26 | 27 | @Override 28 | public void stop() throws Exception { 29 | super.stop(); 30 | Platform.exit(); 31 | System.exit(0); 32 | } 33 | 34 | public static void main(String[] args) { 35 | launch(args); 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/seedu/addressbook/common/Utils.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.common; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | /** 8 | * Utility methods 9 | */ 10 | public class Utils { 11 | 12 | /** 13 | * Checks whether any of the given items are null. 14 | */ 15 | public static boolean isAnyNull(Object... items) { 16 | for (Object item : items) { 17 | if (item == null) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | } 23 | 24 | /** 25 | * Checks if every element in a collection are unique by {@link Object#equals(Object)}. 26 | */ 27 | public static boolean elementsAreUnique(Collection items) { 28 | final Set testSet = new HashSet<>(); 29 | for (Object item : items) { 30 | final boolean itemAlreadyExists = !testSet.add(item); // see Set documentation 31 | if (itemAlreadyExists) { 32 | return false; 33 | } 34 | } 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Software Engineering Education - FOSS Resources 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/CommandResult.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.data.person.ReadOnlyPerson; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | /** 9 | * Represents the result of a command execution. 10 | */ 11 | public class CommandResult { 12 | 13 | /** The feedback message to be shown to the user. Contains a description of the execution result */ 14 | public final String feedbackToUser; 15 | 16 | /** The list of persons that was produced by the command */ 17 | private final List relevantPersons; 18 | 19 | public CommandResult(String feedbackToUser) { 20 | this.feedbackToUser = feedbackToUser; 21 | relevantPersons = null; 22 | } 23 | 24 | public CommandResult(String feedbackToUser, List relevantPersons) { 25 | this.feedbackToUser = feedbackToUser; 26 | this.relevantPersons = relevantPersons; 27 | } 28 | 29 | /** 30 | * Returns list of persons relevant to the command command result, if any. 31 | */ 32 | public Optional> getRelevantPersons() { 33 | return Optional.ofNullable(relevantPersons); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/ViewAllCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.common.Messages; 4 | import seedu.addressbook.data.person.ReadOnlyPerson; 5 | 6 | 7 | /** 8 | * Shows all details of the person identified using the last displayed index. 9 | * Private contact details are shown. 10 | */ 11 | public class ViewAllCommand extends Command { 12 | 13 | public static final String COMMAND_WORD = "viewall"; 14 | 15 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Shows all details of the person " 16 | + "identified by the index number in the last shown person listing.\n\t" 17 | + "Parameters: INDEX\n\t" 18 | + "Example: " + COMMAND_WORD + " 1"; 19 | 20 | public static final String MESSAGE_VIEW_PERSON_DETAILS = "Viewing person: %1$s"; 21 | 22 | 23 | public ViewAllCommand(int targetVisibleIndex) { 24 | super(targetVisibleIndex); 25 | } 26 | 27 | 28 | @Override 29 | public CommandResult execute() { 30 | try { 31 | final ReadOnlyPerson target = getTargetPerson(); 32 | if (!addressBook.containsPerson(target)) { 33 | return new CommandResult(Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK); 34 | } 35 | return new CommandResult(String.format(MESSAGE_VIEW_PERSON_DETAILS, target.getAsTextShowAll())); 36 | } catch (IndexOutOfBoundsException ie) { 37 | return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/ViewCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.common.Messages; 4 | import seedu.addressbook.data.person.ReadOnlyPerson; 5 | 6 | 7 | /** 8 | * Shows details of the person identified using the last displayed index. 9 | * Private contact details are not shown. 10 | */ 11 | public class ViewCommand extends Command { 12 | 13 | public static final String COMMAND_WORD = "view"; 14 | 15 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Shows the non-private details of the person " 16 | + "identified by the index number in the last shown person listing.\n\t" 17 | + "Parameters: INDEX\n\t" 18 | + "Example: " + COMMAND_WORD + " 1"; 19 | 20 | public static final String MESSAGE_VIEW_PERSON_DETAILS = "Viewing person: %1$s"; 21 | 22 | 23 | public ViewCommand(int targetVisibleIndex) { 24 | super(targetVisibleIndex); 25 | } 26 | 27 | 28 | @Override 29 | public CommandResult execute() { 30 | try { 31 | final ReadOnlyPerson target = getTargetPerson(); 32 | if (!addressBook.containsPerson(target)) { 33 | return new CommandResult(Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK); 34 | } 35 | return new CommandResult(String.format(MESSAGE_VIEW_PERSON_DETAILS, target.getAsTextHidePrivate())); 36 | } catch (IndexOutOfBoundsException ie) { 37 | return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/DeleteCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.common.Messages; 4 | import seedu.addressbook.data.person.ReadOnlyPerson; 5 | import seedu.addressbook.data.person.UniquePersonList.PersonNotFoundException; 6 | 7 | 8 | /** 9 | * Deletes a person identified using it's last displayed index from the address book. 10 | */ 11 | public class DeleteCommand extends Command { 12 | 13 | public static final String COMMAND_WORD = "delete"; 14 | 15 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" 16 | + "Deletes the person identified by the index number used in the last person listing.\n\t" 17 | + "Parameters: INDEX\n\t" 18 | + "Example: " + COMMAND_WORD + " 1"; 19 | 20 | public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; 21 | 22 | 23 | public DeleteCommand(int targetVisibleIndex) { 24 | super(targetVisibleIndex); 25 | } 26 | 27 | 28 | @Override 29 | public CommandResult execute() { 30 | try { 31 | final ReadOnlyPerson target = getTargetPerson(); 32 | addressBook.removePerson(target); 33 | return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, target)); 34 | 35 | } catch (IndexOutOfBoundsException ie) { 36 | return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); 37 | } catch (PersonNotFoundException pnfe) { 38 | return new CommandResult(Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/tag/Tag.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.tag; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | 5 | /** 6 | * Represents a Tag in the address book. 7 | * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} 8 | */ 9 | public class Tag { 10 | 11 | public static final String MESSAGE_TAG_CONSTRAINTS = "Tags names should be alphanumeric"; 12 | public static final String TAG_VALIDATION_REGEX = "\\p{Alnum}+"; 13 | 14 | public final String tagName; 15 | 16 | /** 17 | * Validates given tag name. 18 | * 19 | * @throws IllegalValueException if the given tag name string is invalid. 20 | */ 21 | public Tag(String name) throws IllegalValueException { 22 | name = name.trim(); 23 | if (!isValidTagName(name)) { 24 | throw new IllegalValueException(MESSAGE_TAG_CONSTRAINTS); 25 | } 26 | this.tagName = name; 27 | } 28 | 29 | /** 30 | * Returns true if a given string is a valid tag name. 31 | */ 32 | public static boolean isValidTagName(String test) { 33 | return test.matches(TAG_VALIDATION_REGEX); 34 | } 35 | 36 | @Override 37 | public boolean equals(Object other) { 38 | return other == this // short circuit if same object 39 | || (other instanceof Tag // instanceof handles nulls 40 | && this.tagName.equals(((Tag) other).tagName)); // state check 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return tagName.hashCode(); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return '[' + tagName + ']'; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/seedu/addressbook/storage/jaxb/AdaptedTag.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.storage.jaxb; 2 | 3 | import seedu.addressbook.common.Utils; 4 | import seedu.addressbook.data.exception.IllegalValueException; 5 | import seedu.addressbook.data.tag.Tag; 6 | 7 | import javax.xml.bind.annotation.XmlValue; 8 | 9 | /** 10 | * JAXB-friendly adapted tag data holder class. 11 | */ 12 | public class AdaptedTag { 13 | 14 | @XmlValue 15 | public String tagName; 16 | 17 | /** 18 | * No-arg constructor for JAXB use. 19 | */ 20 | public AdaptedTag() {} 21 | 22 | /** 23 | * Converts a given Tag into this class for JAXB use. 24 | * 25 | * @param source future changes to this will not affect the created AdaptedTag 26 | */ 27 | public AdaptedTag(Tag source) { 28 | tagName = source.tagName; 29 | } 30 | 31 | /** 32 | * Returns true if any required field is missing. 33 | * 34 | * JAXB does not enforce (required = true) without a given XML schema. 35 | * Since we do most of our validation using the data class constructors, the only extra logic we need 36 | * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, 37 | * so we check for that. 38 | */ 39 | public boolean isAnyRequiredFieldMissing() { 40 | return Utils.isAnyNull(tagName); 41 | } 42 | 43 | /** 44 | * Converts this jaxb-friendly adapted tag object into the Tag object. 45 | * 46 | * @throws IllegalValueException if there were any data constraints violated in the adapted person 47 | */ 48 | public Tag toModelType() throws IllegalValueException { 49 | return new Tag(tagName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/Address.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | 5 | /** 6 | * Represents a Person's address in the address book. 7 | * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} 8 | */ 9 | public class Address { 10 | 11 | public static final String EXAMPLE = "123, some street"; 12 | public static final String MESSAGE_ADDRESS_CONSTRAINTS = "Person addresses can be in any format"; 13 | public static final String ADDRESS_VALIDATION_REGEX = ".+"; 14 | 15 | public final String value; 16 | private boolean isPrivate; 17 | 18 | /** 19 | * Validates given address. 20 | * 21 | * @throws IllegalValueException if given address string is invalid. 22 | */ 23 | public Address(String address, boolean isPrivate) throws IllegalValueException { 24 | this.isPrivate = isPrivate; 25 | if (!isValidAddress(address)) { 26 | throw new IllegalValueException(MESSAGE_ADDRESS_CONSTRAINTS); 27 | } 28 | this.value = address; 29 | } 30 | 31 | /** 32 | * Returns true if a given string is a valid person email. 33 | */ 34 | public static boolean isValidAddress(String test) { 35 | return test.matches(ADDRESS_VALIDATION_REGEX); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return value; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object other) { 45 | return other == this // short circuit if same object 46 | || (other instanceof Address // instanceof handles nulls 47 | && this.value.equals(((Address) other).value)); // state check 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return value.hashCode(); 53 | } 54 | 55 | public boolean isPrivate() { 56 | return isPrivate; 57 | } 58 | } -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/Gui.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.ui; 2 | 3 | import javafx.fxml.FXMLLoader; 4 | import javafx.scene.Scene; 5 | import javafx.stage.Stage; 6 | import seedu.addressbook.logic.Logic; 7 | import seedu.addressbook.Main; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | /** 13 | * The GUI of the App 14 | */ 15 | public class Gui { 16 | 17 | /** Offset required to convert between 1-indexing and 0-indexing. */ 18 | public static final int DISPLAYED_INDEX_OFFSET = 1; 19 | 20 | public static final int INITIAL_WINDOW_WIDTH = 800; 21 | public static final int INITIAL_WINDOW_HEIGHT = 600; 22 | private final Logic logic; 23 | 24 | private MainWindow mainWindow; 25 | private String version; 26 | 27 | public Gui(Logic logic, String version) { 28 | this.logic = logic; 29 | this.version = version; 30 | } 31 | 32 | public void start(Stage stage, Stoppable mainApp) throws IOException { 33 | mainWindow = createMainWindow(stage, mainApp); 34 | mainWindow.displayWelcomeMessage(version, logic.getStorageFilePath()); 35 | } 36 | 37 | private MainWindow createMainWindow(Stage stage, Stoppable mainApp) throws IOException{ 38 | FXMLLoader loader = new FXMLLoader(); 39 | 40 | /* Note: When calling getResource(), use '/', instead of File.separator or '\\' 41 | * More info: http://docs.oracle.com/javase/8/docs/technotes/guides/lang/resources.html#res_name_context 42 | */ 43 | loader.setLocation(Main.class.getResource("ui/mainwindow.fxml")); 44 | 45 | stage.setTitle(version); 46 | stage.setScene(new Scene(loader.load(), INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)); 47 | stage.show(); 48 | MainWindow mainWindow = loader.getController(); 49 | mainWindow.setLogic(logic); 50 | mainWindow.setMainApp(mainApp); 51 | return mainWindow; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/Phone.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | 5 | /** 6 | * Represents a Person's phone number in the address book. 7 | * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} 8 | */ 9 | public class Phone { 10 | 11 | public static final String EXAMPLE = "123456789"; 12 | public static final String MESSAGE_PHONE_CONSTRAINTS = "Person phone numbers should only contain numbers"; 13 | public static final String PHONE_VALIDATION_REGEX = "\\d+"; 14 | 15 | public final String value; 16 | private boolean isPrivate; 17 | 18 | /** 19 | * Validates given phone number. 20 | * 21 | * @throws IllegalValueException if given phone string is invalid. 22 | */ 23 | public Phone(String phone, boolean isPrivate) throws IllegalValueException { 24 | this.isPrivate = isPrivate; 25 | phone = phone.trim(); 26 | if (!isValidPhone(phone)) { 27 | throw new IllegalValueException(MESSAGE_PHONE_CONSTRAINTS); 28 | } 29 | this.value = phone; 30 | } 31 | 32 | /** 33 | * Checks if a given string is a valid person phone number. 34 | */ 35 | public static boolean isValidPhone(String test) { 36 | return test.matches(PHONE_VALIDATION_REGEX); 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return value; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object other) { 46 | return other == this // short circuit if same object 47 | || (other instanceof Phone // instanceof handles nulls 48 | && this.value.equals(((Phone) other).value)); // state check 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return value.hashCode(); 54 | } 55 | 56 | public boolean isPrivate() { 57 | return isPrivate; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/se-edu/addressbook-level3.svg?branch=master)](https://travis-ci.org/se-edu/addressbook-level3) 2 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/d4a0954383444a8db8cb26e5f5b7302c)](https://www.codacy.com/app/se-edu/addressbook-level3?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level3&utm_campaign=Badge_Grade) 3 | 4 | # AddressBook (Level 3) 5 | 6 | 7 | 8 | * This is a CLI (Command Line Interface) Address Book application **written in OOP fashion**. It has a very basic GUI. 9 | * It is a Java sample application intended for students learning Software Engineering while using Java as 10 | the main programming language. 11 | * It provides a **reasonably well-written** code example that is **significantly bigger** than what students 12 | usually write in data structure modules. 13 | 14 | **What's different from level 2** 15 | 16 | * A simple GUI added to replace the Text UI. 17 | * A `Logic` class added together with a `LogicTest` class. 18 | * Appendices added to [Developer Guide](doc/DeveloperGuide.md). 19 | 20 | 21 | **Useful Links** 22 | * [User Guide](doc/UserGuide.md) 23 | * [Developer Guide](doc/DeveloperGuide.md) 24 | * [Learning Outcomes](doc/LearningOutcomes.md) 25 | 26 | # Contributors 27 | 28 | The full list of contributors for se-edu can be found [here](https://se-edu.github.io/docs/Team.html). 29 | 30 | # Acknowledgements 31 | 32 | Some parts of this sample application was inspired by the excellent 33 | [Java FX tutorial](http://code.makery.ch/library/javafx-8-tutorial/) by Marco Jakob 34 | 35 | # Contact Us 36 | 37 | * **Bug reports, Suggestions** : Post in our [issue tracker](https://github.com/se-edu/addressbook-level3/issues) 38 | if you noticed bugs or have suggestions on how to improve. 39 | * **Contributing** : We welcome pull requests. Follow the process described [here](https://github.com/oss-generic/process) 40 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/Email.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | 5 | /** 6 | * Represents a Person's email in the address book. 7 | * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} 8 | */ 9 | public class Email { 10 | 11 | public static final String EXAMPLE = "valid@e.mail"; 12 | public static final String MESSAGE_EMAIL_CONSTRAINTS = 13 | "Person emails should be 2 alphanumeric/period strings separated by '@'"; 14 | public static final String EMAIL_VALIDATION_REGEX = "[\\w\\.]+@[\\w\\.]+"; 15 | 16 | public final String value; 17 | private boolean isPrivate; 18 | 19 | /** 20 | * Validates given email. 21 | * 22 | * @throws IllegalValueException if given email address string is invalid. 23 | */ 24 | public Email(String email, boolean isPrivate) throws IllegalValueException { 25 | this.isPrivate = isPrivate; 26 | email = email.trim(); 27 | if (!isValidEmail(email)) { 28 | throw new IllegalValueException(MESSAGE_EMAIL_CONSTRAINTS); 29 | } 30 | this.value = email; 31 | } 32 | 33 | /** 34 | * Checks if a given string is a valid person email. 35 | */ 36 | public static boolean isValidEmail(String test) { 37 | return test.matches(EMAIL_VALIDATION_REGEX); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return value; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object other) { 47 | return other == this // short circuit if same object 48 | || (other instanceof Email // instanceof handles nulls 49 | && this.value.equals(((Email) other).value)); // state check 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return value.hashCode(); 55 | } 56 | 57 | 58 | public boolean isPrivate() { 59 | return isPrivate; 60 | } 61 | } -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/Name.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | /** 9 | * Represents a Person's name in the address book. 10 | * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} 11 | */ 12 | public class Name { 13 | 14 | public static final String EXAMPLE = "John Doe"; 15 | public static final String MESSAGE_NAME_CONSTRAINTS = "Person names should be spaces or alphanumeric characters"; 16 | public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum} ]+"; 17 | 18 | public final String fullName; 19 | 20 | /** 21 | * Validates given name. 22 | * 23 | * @throws IllegalValueException if given name string is invalid. 24 | */ 25 | public Name(String name) throws IllegalValueException { 26 | name = name.trim(); 27 | if (!isValidName(name)) { 28 | throw new IllegalValueException(MESSAGE_NAME_CONSTRAINTS); 29 | } 30 | this.fullName = name; 31 | } 32 | 33 | /** 34 | * Returns true if a given string is a valid person name. 35 | */ 36 | public static boolean isValidName(String test) { 37 | return test.matches(NAME_VALIDATION_REGEX); 38 | } 39 | 40 | /** 41 | * Retrieves a listing of every word in the name, in order. 42 | */ 43 | public List getWordsInName() { 44 | return Arrays.asList(fullName.split("\\s+")); 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return fullName; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object other) { 54 | return other == this // short circuit if same object 55 | || (other instanceof Name // instanceof handles nulls 56 | && this.fullName.equals(((Name) other).fullName)); // state check 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return fullName.hashCode(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/FindCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.data.person.ReadOnlyPerson; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * Finds and lists all persons in address book whose name contains any of the argument keywords. 9 | * Keyword matching is case sensitive. 10 | */ 11 | public class FindCommand extends Command { 12 | 13 | public static final String COMMAND_WORD = "find"; 14 | 15 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Finds all persons whose names contain any of " 16 | + "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n\t" 17 | + "Parameters: KEYWORD [MORE_KEYWORDS]...\n\t" 18 | + "Example: " + COMMAND_WORD + " alice bob charlie"; 19 | 20 | private final Set keywords; 21 | 22 | public FindCommand(Set keywords) { 23 | this.keywords = keywords; 24 | } 25 | 26 | /** 27 | * Returns copy of keywords in this command. 28 | */ 29 | public Set getKeywords() { 30 | return new HashSet<>(keywords); 31 | } 32 | 33 | @Override 34 | public CommandResult execute() { 35 | final List personsFound = getPersonsWithNameContainingAnyKeyword(keywords); 36 | return new CommandResult(getMessageForPersonListShownSummary(personsFound), personsFound); 37 | } 38 | 39 | /** 40 | * Retrieve all persons in the address book whose names contain some of the specified keywords. 41 | * 42 | * @param keywords for searching 43 | * @return list of persons found 44 | */ 45 | private List getPersonsWithNameContainingAnyKeyword(Set keywords) { 46 | final List matchedPersons = new ArrayList<>(); 47 | for (ReadOnlyPerson person : addressBook.getAllPersons()) { 48 | final Set wordsInName = new HashSet<>(person.getName().getWordsInName()); 49 | if (!Collections.disjoint(wordsInName, keywords)) { 50 | matchedPersons.add(person); 51 | } 52 | } 53 | return matchedPersons; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/Formatter.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.ui; 2 | 3 | import seedu.addressbook.data.person.ReadOnlyPerson; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Used for formatting text for display. e.g. for adding text decorations. 10 | */ 11 | public class Formatter { 12 | 13 | /** A decorative prefix added to the beginning of lines printed by AddressBook */ 14 | private static final String LINE_PREFIX = " "; 15 | 16 | /** A platform independent line separator. */ 17 | private static final String LS = System.lineSeparator(); 18 | 19 | 20 | /** Format of indexed list item */ 21 | private static final String MESSAGE_INDEXED_LIST_ITEM = "\t%1$d. %2$s"; 22 | 23 | 24 | /** Offset required to convert between 1-indexing and 0-indexing. */ 25 | private static final int DISPLAYED_INDEX_OFFSET = 1; 26 | 27 | 28 | /** Formats the given strings for displaying to the user. */ 29 | public String format(String... messages) { 30 | StringBuilder sb = new StringBuilder(); 31 | for (String m : messages) { 32 | sb.append(LINE_PREFIX + m.replace("\n", LS + LINE_PREFIX) + LS); 33 | } 34 | return sb.toString(); 35 | } 36 | 37 | /** Formats the given list of persons for displaying to the user. */ 38 | public String format(List persons) { 39 | final List formattedPersons = new ArrayList<>(); 40 | for (ReadOnlyPerson person : persons) { 41 | formattedPersons.add(person.getAsTextHidePrivate()); 42 | } 43 | return format(asIndexedList(formattedPersons)); 44 | } 45 | 46 | /** Formats a list of strings as an indexed list. */ 47 | private static String asIndexedList(List listItems) { 48 | final StringBuilder formatted = new StringBuilder(); 49 | int displayIndex = 0 + DISPLAYED_INDEX_OFFSET; 50 | for (String listItem : listItems) { 51 | formatted.append(getIndexedListItem(displayIndex, listItem)).append("\n"); 52 | displayIndex++; 53 | } 54 | return formatted.toString(); 55 | } 56 | 57 | /** 58 | * Formats a string as an indexed list item. 59 | * 60 | * @param visibleIndex index for this listing 61 | */ 62 | private static String getIndexedListItem(int visibleIndex, String listItem) { 63 | return String.format(MESSAGE_INDEXED_LIST_ITEM, visibleIndex, listItem); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/Person.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.tag.UniqueTagList; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * Represents a Person in the address book. 9 | * Guarantees: details are present and not null, field values are validated. 10 | */ 11 | public class Person implements ReadOnlyPerson { 12 | 13 | private Name name; 14 | private Phone phone; 15 | private Email email; 16 | private Address address; 17 | 18 | private final UniqueTagList tags; 19 | /** 20 | * Assumption: Every field must be present and not null. 21 | */ 22 | public Person(Name name, Phone phone, Email email, Address address, UniqueTagList tags) { 23 | this.name = name; 24 | this.phone = phone; 25 | this.email = email; 26 | this.address = address; 27 | this.tags = new UniqueTagList(tags); // protect internal tags from changes in the arg list 28 | } 29 | 30 | /** 31 | * Copy constructor. 32 | */ 33 | public Person(ReadOnlyPerson source) { 34 | this(source.getName(), source.getPhone(), source.getEmail(), source.getAddress(), source.getTags()); 35 | } 36 | 37 | @Override 38 | public Name getName() { 39 | return name; 40 | } 41 | 42 | @Override 43 | public Phone getPhone() { 44 | return phone; 45 | } 46 | 47 | @Override 48 | public Email getEmail() { 49 | return email; 50 | } 51 | 52 | @Override 53 | public Address getAddress() { 54 | return address; 55 | } 56 | 57 | @Override 58 | public UniqueTagList getTags() { 59 | return new UniqueTagList(tags); 60 | } 61 | 62 | /** 63 | * Replaces this person's tags with the tags in the argument tag list. 64 | */ 65 | public void setTags(UniqueTagList replacement) { 66 | tags.setTags(replacement); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object other) { 71 | return other == this // short circuit if same object 72 | || (other instanceof ReadOnlyPerson // instanceof handles nulls 73 | && this.isSameStateAs((ReadOnlyPerson) other)); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | // use this method for custom fields hashing instead of implementing your own 79 | return Objects.hash(name, phone, email, address, tags); 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return getAsTextShowAll(); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /test/java/seedu/addressbook/common/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.common; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import org.junit.Test; 10 | 11 | public class UtilsTest { 12 | @Test 13 | public void isAnyNull() { 14 | // empty list 15 | assertFalse(Utils.isAnyNull()); 16 | 17 | // Any non-empty list 18 | assertFalse(Utils.isAnyNull(new Object(), new Object())); 19 | assertFalse(Utils.isAnyNull("test")); 20 | assertFalse(Utils.isAnyNull("")); 21 | 22 | // non empty list with just one null at the beginning 23 | assertTrue(Utils.isAnyNull((Object) null)); 24 | assertTrue(Utils.isAnyNull(null, "", new Object())); 25 | assertTrue(Utils.isAnyNull(null, new Object(), new Object())); 26 | 27 | // non empty list with nulls in the middle 28 | assertTrue(Utils.isAnyNull(new Object(), null, null, "test")); 29 | assertTrue(Utils.isAnyNull("", null, new Object())); 30 | 31 | // non empty list with one null as the last element 32 | assertTrue(Utils.isAnyNull("", new Object(), null)); 33 | assertTrue(Utils.isAnyNull(new Object(), new Object(), null)); 34 | 35 | // confirms nulls inside the list are not considered 36 | List nullList = Arrays.asList((Object) null); 37 | assertFalse(Utils.isAnyNull(nullList)); 38 | } 39 | 40 | @Test 41 | public void elementsAreUnique() throws Exception { 42 | // empty list 43 | assertAreUnique(); 44 | 45 | // only one object 46 | assertAreUnique((Object) null); 47 | assertAreUnique(1); 48 | assertAreUnique(""); 49 | assertAreUnique("abc"); 50 | 51 | // all objects unique 52 | assertAreUnique("abc", "ab", "a"); 53 | assertAreUnique(1, 2); 54 | 55 | // some identical objects 56 | assertNotUnique("abc", "abc"); 57 | assertNotUnique("abc", "", "abc", "ABC"); 58 | assertNotUnique("", "abc", "a", "abc"); 59 | assertNotUnique(1, new Integer(1)); 60 | assertNotUnique(null, 1, new Integer(1)); 61 | assertNotUnique(null, null); 62 | assertNotUnique(null, "a", "b", null); 63 | } 64 | 65 | private void assertAreUnique(Object... objects) { 66 | assertTrue(Utils.elementsAreUnique(Arrays.asList(objects))); 67 | } 68 | 69 | private void assertNotUnique(Object... objects) { 70 | assertFalse(Utils.elementsAreUnique(Arrays.asList(objects))); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/Command.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.common.Messages; 4 | import seedu.addressbook.data.AddressBook; 5 | import seedu.addressbook.data.person.ReadOnlyPerson; 6 | 7 | import java.util.List; 8 | 9 | import static seedu.addressbook.ui.Gui.DISPLAYED_INDEX_OFFSET; 10 | 11 | /** 12 | * Represents an executable command. 13 | */ 14 | public abstract class Command { 15 | protected AddressBook addressBook; 16 | protected List relevantPersons; 17 | private int targetIndex = -1; 18 | 19 | /** 20 | * @param targetIndex last visible listing index of the target person 21 | */ 22 | public Command(int targetIndex) { 23 | this.setTargetIndex(targetIndex); 24 | } 25 | 26 | protected Command() { 27 | } 28 | 29 | /** 30 | * Constructs a feedback message to summarise an operation that displayed a listing of persons. 31 | * 32 | * @param personsDisplayed used to generate summary 33 | * @return summary message for persons displayed 34 | */ 35 | public static String getMessageForPersonListShownSummary(List personsDisplayed) { 36 | return String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, personsDisplayed.size()); 37 | } 38 | 39 | /** 40 | * Executes the command and returns the result. 41 | */ 42 | public CommandResult execute(){ 43 | throw new UnsupportedOperationException("This method should be implement in child classes"); 44 | } 45 | 46 | //Note: it is better to make the execute() method abstract, by replacing the above method with the line below: 47 | //public abstract CommandResult execute(); 48 | 49 | /** 50 | * Supplies the data the command will operate on. 51 | */ 52 | public void setData(AddressBook addressBook, List relevantPersons) { 53 | this.addressBook = addressBook; 54 | this.relevantPersons = relevantPersons; 55 | } 56 | 57 | /** 58 | * Extracts the the target person in the last shown list from the given arguments. 59 | * 60 | * @throws IndexOutOfBoundsException if the target index is out of bounds of the last viewed listing 61 | */ 62 | protected ReadOnlyPerson getTargetPerson() throws IndexOutOfBoundsException { 63 | return relevantPersons.get(getTargetIndex() - DISPLAYED_INDEX_OFFSET); 64 | } 65 | 66 | public int getTargetIndex() { 67 | return targetIndex; 68 | } 69 | 70 | public void setTargetIndex(int targetIndex) { 71 | this.targetIndex = targetIndex; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/seedu/addressbook/commands/AddCommand.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.commands; 2 | 3 | import seedu.addressbook.data.exception.IllegalValueException; 4 | import seedu.addressbook.data.person.*; 5 | import seedu.addressbook.data.tag.Tag; 6 | import seedu.addressbook.data.tag.UniqueTagList; 7 | 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | /** 12 | * Adds a person to the address book. 13 | */ 14 | public class AddCommand extends Command { 15 | 16 | public static final String COMMAND_WORD = "add"; 17 | 18 | public static final String MESSAGE_USAGE = COMMAND_WORD + ":\n" + "Adds a person to the address book. " 19 | + "Contact details can be marked private by prepending 'p' to the prefix.\n\t" 20 | + "Parameters: NAME [p]p/PHONE [p]e/EMAIL [p]a/ADDRESS [t/TAG]...\n\t" 21 | + "Example: " + COMMAND_WORD 22 | + " John Doe p/98765432 e/johnd@gmail.com a/311, Clementi Ave 2, #02-25 t/friends t/owesMoney"; 23 | 24 | public static final String MESSAGE_SUCCESS = "New person added: %1$s"; 25 | public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; 26 | 27 | private final Person toAdd; 28 | 29 | /** 30 | * Convenience constructor using raw values. 31 | * 32 | * @throws IllegalValueException if any of the raw values are invalid 33 | */ 34 | public AddCommand(String name, 35 | String phone, boolean isPhonePrivate, 36 | String email, boolean isEmailPrivate, 37 | String address, boolean isAddressPrivate, 38 | Set tags) throws IllegalValueException { 39 | final Set tagSet = new HashSet<>(); 40 | for (String tagName : tags) { 41 | tagSet.add(new Tag(tagName)); 42 | } 43 | this.toAdd = new Person( 44 | new Name(name), 45 | new Phone(phone, isPhonePrivate), 46 | new Email(email, isEmailPrivate), 47 | new Address(address, isAddressPrivate), 48 | new UniqueTagList(tagSet) 49 | ); 50 | } 51 | 52 | public AddCommand(Person toAdd) { 53 | this.toAdd = toAdd; 54 | } 55 | 56 | public ReadOnlyPerson getPerson() { 57 | return toAdd; 58 | } 59 | 60 | @Override 61 | public CommandResult execute() { 62 | try { 63 | addressBook.addPerson(toAdd); 64 | return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); 65 | } catch (UniquePersonList.DuplicatePersonException dpe) { 66 | return new CommandResult(MESSAGE_DUPLICATE_PERSON); 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/ReadOnlyPerson.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.data.tag.Tag; 4 | import seedu.addressbook.data.tag.UniqueTagList; 5 | 6 | /** 7 | * A read-only immutable interface for a Person in the addressbook. 8 | * Implementations should guarantee: details are present and not null, field values are validated. 9 | */ 10 | public interface ReadOnlyPerson { 11 | 12 | Name getName(); 13 | Phone getPhone(); 14 | Email getEmail(); 15 | Address getAddress(); 16 | 17 | /** 18 | * The returned TagList is a deep copy of the internal TagList, 19 | * changes on the returned list will not affect the person's internal tags. 20 | */ 21 | UniqueTagList getTags(); 22 | 23 | /** 24 | * Returns true if the values inside this object is same as those of the other (Note: interfaces cannot override .equals) 25 | */ 26 | default boolean isSameStateAs(ReadOnlyPerson other) { 27 | return other == this // short circuit if same object 28 | || (other != null // this is first to avoid NPE below 29 | && other.getName().equals(this.getName()) // state checks here onwards 30 | && other.getPhone().equals(this.getPhone()) 31 | && other.getEmail().equals(this.getEmail()) 32 | && other.getAddress().equals(this.getAddress())); 33 | } 34 | 35 | /** 36 | * Formats the person as text, showing all contact details. 37 | */ 38 | default String getAsTextShowAll() { 39 | final StringBuilder builder = new StringBuilder(); 40 | final String detailIsPrivate = "(private) "; 41 | builder.append(getName()) 42 | .append(" Phone: "); 43 | if (getPhone().isPrivate()) { 44 | builder.append(detailIsPrivate); 45 | } 46 | builder.append(getPhone()) 47 | .append(" Email: "); 48 | if (getEmail().isPrivate()) { 49 | builder.append(detailIsPrivate); 50 | } 51 | builder.append(getEmail()) 52 | .append(" Address: "); 53 | if (getAddress().isPrivate()) { 54 | builder.append(detailIsPrivate); 55 | } 56 | builder.append(getAddress()) 57 | .append(" Tags: "); 58 | for (Tag tag : getTags()) { 59 | builder.append(tag); 60 | } 61 | return builder.toString(); 62 | } 63 | 64 | /** 65 | * Formats a person as text, showing only non-private contact details. 66 | */ 67 | default String getAsTextHidePrivate() { 68 | final StringBuilder builder = new StringBuilder(); 69 | builder.append(getName()); 70 | if (!getPhone().isPrivate()) { 71 | builder.append(" Phone: ").append(getPhone()); 72 | } 73 | if (!getEmail().isPrivate()) { 74 | builder.append(" Email: ").append(getEmail()); 75 | } 76 | if (!getAddress().isPrivate()) { 77 | builder.append(" Address: ").append(getAddress()); 78 | } 79 | builder.append(" Tags: "); 80 | for (Tag tag : getTags()) { 81 | builder.append(tag); 82 | } 83 | return builder.toString(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/seedu/addressbook/storage/jaxb/AdaptedAddressBook.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.storage.jaxb; 2 | 3 | import seedu.addressbook.data.AddressBook; 4 | import seedu.addressbook.data.exception.IllegalValueException; 5 | import seedu.addressbook.data.person.Person; 6 | import seedu.addressbook.data.person.ReadOnlyPerson; 7 | import seedu.addressbook.data.person.UniquePersonList; 8 | import seedu.addressbook.data.tag.Tag; 9 | import seedu.addressbook.data.tag.UniqueTagList; 10 | 11 | import javax.xml.bind.annotation.XmlElement; 12 | import javax.xml.bind.annotation.XmlRootElement; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * JAXB-friendly adapted address book data holder class. 18 | */ 19 | @XmlRootElement(name = "AddressBook") 20 | public class AdaptedAddressBook { 21 | 22 | @XmlElement 23 | private List persons = new ArrayList<>(); 24 | @XmlElement 25 | private List tags = new ArrayList<>(); 26 | 27 | /** 28 | * No-arg constructor for JAXB use. 29 | */ 30 | public AdaptedAddressBook() {} 31 | 32 | /** 33 | * Converts a given AddressBook into this class for JAXB use. 34 | * 35 | * @param source future changes to this will not affect the created AdaptedAddressBook 36 | */ 37 | public AdaptedAddressBook(AddressBook source) { 38 | persons = new ArrayList<>(); 39 | tags = new ArrayList<>(); 40 | for (ReadOnlyPerson person : source.getAllPersons()) { 41 | persons.add(new AdaptedPerson(person)); 42 | } 43 | for (Tag tag : source.getAllTags()) { 44 | tags.add(new AdaptedTag(tag)); 45 | } 46 | } 47 | 48 | 49 | /** 50 | * Returns true if any required field is missing. 51 | * 52 | * JAXB does not enforce (required = true) without a given XML schema. 53 | * Since we do most of our validation using the data class constructors, the only extra logic we need 54 | * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, 55 | * so we check for that. 56 | */ 57 | public boolean isAnyRequiredFieldMissing() { 58 | for (AdaptedTag tag : tags) { 59 | if (tag.isAnyRequiredFieldMissing()) { 60 | return true; 61 | } 62 | } 63 | for (AdaptedPerson person : persons) { 64 | if (person.isAnyRequiredFieldMissing()) { 65 | return true; 66 | } 67 | } 68 | return false; 69 | } 70 | 71 | 72 | /** 73 | * Converts this jaxb-friendly {@code AdaptedAddressBook} object into the corresponding(@code AddressBook} object. 74 | * @throws IllegalValueException if there were any data constraints violated in the adapted person 75 | */ 76 | public AddressBook toModelType() throws IllegalValueException { 77 | final List tagList = new ArrayList<>(); 78 | final List personList = new ArrayList<>(); 79 | for (AdaptedTag tag : tags) { 80 | tagList.add(tag.toModelType()); 81 | } 82 | for (AdaptedPerson person : persons) { 83 | personList.add(person.toModelType()); 84 | } 85 | return new AddressBook(new UniquePersonList(personList), new UniqueTagList(tagList)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/seedu/addressbook/logic/Logic.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.logic; 2 | 3 | import seedu.addressbook.commands.Command; 4 | import seedu.addressbook.commands.CommandResult; 5 | import seedu.addressbook.data.AddressBook; 6 | import seedu.addressbook.data.person.ReadOnlyPerson; 7 | import seedu.addressbook.parser.Parser; 8 | import seedu.addressbook.storage.StorageFile; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | /** 15 | * Represents the main Logic of the AddressBook. 16 | */ 17 | public class Logic { 18 | 19 | 20 | private StorageFile storage; 21 | private AddressBook addressBook; 22 | 23 | /** The list of person shown to the user most recently. */ 24 | private List lastShownList = Collections.emptyList(); 25 | 26 | public Logic() throws Exception{ 27 | setStorage(initializeStorage()); 28 | setAddressBook(storage.load()); 29 | } 30 | 31 | Logic(StorageFile storageFile, AddressBook addressBook){ 32 | setStorage(storageFile); 33 | setAddressBook(addressBook); 34 | } 35 | 36 | void setStorage(StorageFile storage){ 37 | this.storage = storage; 38 | } 39 | 40 | void setAddressBook(AddressBook addressBook){ 41 | this.addressBook = addressBook; 42 | } 43 | 44 | /** 45 | * Creates the StorageFile object based on the user specified path (if any) or the default storage path. 46 | * @throws StorageFile.InvalidStorageFilePathException if the target file path is incorrect. 47 | */ 48 | private StorageFile initializeStorage() throws StorageFile.InvalidStorageFilePathException { 49 | return new StorageFile(); 50 | } 51 | 52 | public String getStorageFilePath() { 53 | return storage.getPath(); 54 | } 55 | 56 | /** 57 | * Unmodifiable view of the current last shown list. 58 | */ 59 | public List getLastShownList() { 60 | return Collections.unmodifiableList(lastShownList); 61 | } 62 | 63 | protected void setLastShownList(List newList) { 64 | lastShownList = newList; 65 | } 66 | 67 | /** 68 | * Parses the user command, executes it, and returns the result. 69 | * @throws Exception if there was any problem during command execution. 70 | */ 71 | public CommandResult execute(String userCommandText) throws Exception { 72 | Command command = new Parser().parseCommand(userCommandText); 73 | CommandResult result = execute(command); 74 | recordResult(result); 75 | return result; 76 | } 77 | 78 | /** 79 | * Executes the command, updates storage, and returns the result. 80 | * 81 | * @param command user command 82 | * @return result of the command 83 | * @throws Exception if there was any problem during command execution. 84 | */ 85 | private CommandResult execute(Command command) throws Exception { 86 | command.setData(addressBook, lastShownList); 87 | CommandResult result = command.execute(); 88 | storage.save(addressBook); 89 | return result; 90 | } 91 | 92 | /** Updates the {@link #lastShownList} if the result contains a list of Persons. */ 93 | private void recordResult(CommandResult result) { 94 | final Optional> personList = result.getRelevantPersons(); 95 | if (personList.isPresent()) { 96 | lastShownList = personList.get(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/seedu/addressbook/ui/MainWindow.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.ui; 2 | 3 | 4 | import javafx.event.ActionEvent; 5 | import javafx.fxml.FXML; 6 | import javafx.scene.control.TextArea; 7 | import javafx.scene.control.TextField; 8 | import seedu.addressbook.commands.ExitCommand; 9 | import seedu.addressbook.logic.Logic; 10 | import seedu.addressbook.commands.CommandResult; 11 | import seedu.addressbook.data.person.ReadOnlyPerson; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | import static seedu.addressbook.common.Messages.*; 17 | 18 | /** 19 | * Main Window of the GUI. 20 | */ 21 | public class MainWindow { 22 | 23 | private Logic logic; 24 | private Stoppable mainApp; 25 | 26 | public MainWindow(){ 27 | } 28 | 29 | public void setLogic(Logic logic){ 30 | this.logic = logic; 31 | } 32 | 33 | public void setMainApp(Stoppable mainApp){ 34 | this.mainApp = mainApp; 35 | } 36 | 37 | @FXML 38 | private TextArea outputConsole; 39 | 40 | @FXML 41 | private TextField commandInput; 42 | 43 | 44 | @FXML 45 | void onCommand(ActionEvent event) { 46 | try { 47 | String userCommandText = commandInput.getText(); 48 | CommandResult result = logic.execute(userCommandText); 49 | if(isExitCommand(result)){ 50 | exitApp(); 51 | return; 52 | } 53 | displayResult(result); 54 | clearCommandInput(); 55 | } catch (Exception e) { 56 | display(e.getMessage()); 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | private void exitApp() throws Exception { 62 | mainApp.stop(); 63 | } 64 | 65 | /** Returns true of the result given is the result of an exit command */ 66 | private boolean isExitCommand(CommandResult result) { 67 | return result.feedbackToUser.equals(ExitCommand.MESSAGE_EXIT_ACKNOWEDGEMENT); 68 | } 69 | 70 | /** Clears the command input box */ 71 | private void clearCommandInput() { 72 | commandInput.setText(""); 73 | } 74 | 75 | /** Clears the output display area */ 76 | public void clearOutputConsole(){ 77 | outputConsole.clear(); 78 | } 79 | 80 | /** Displays the result of a command execution to the user. */ 81 | public void displayResult(CommandResult result) { 82 | clearOutputConsole(); 83 | final Optional> resultPersons = result.getRelevantPersons(); 84 | if(resultPersons.isPresent()) { 85 | display(resultPersons.get()); 86 | } 87 | display(result.feedbackToUser); 88 | } 89 | 90 | public void displayWelcomeMessage(String version, String storageFilePath) { 91 | String storageFileInfo = String.format(MESSAGE_USING_STORAGE_FILE, storageFilePath); 92 | display(MESSAGE_WELCOME, version, MESSAGE_PROGRAM_LAUNCH_ARGS_USAGE, storageFileInfo); 93 | } 94 | 95 | /** 96 | * Displays the list of persons in the output display area, formatted as an indexed list. 97 | * Private contact details are hidden. 98 | */ 99 | private void display(List persons) { 100 | display(new Formatter().format(persons)); 101 | } 102 | 103 | /** 104 | * Displays the given messages on the output display area, after formatting appropriately. 105 | */ 106 | private void display(String... messages) { 107 | outputConsole.setText(outputConsole.getText() + new Formatter().format(messages)); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /doc/UserGuide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | This product is not meant for end-users and therefore there is no user-friendly installer. 4 | Please refer to the [Setting up](DeveloperGuide.md#setting-up) section to learn how to set up the project. 5 | 6 | ## Starting the program 7 | 8 | 1. Find the project pane (usually located at the left side) 9 | 2. Open up `src/seedu.addressbook` folder 10 | 3. Right click on `Main` 11 | 4. Click `Run Main.main()` 12 | 5. The GUI should appear in a few seconds 13 | 14 | 15 | 16 | ## Viewing help : `help` 17 | Format: `help` 18 | 19 | > Help is also shown if you enter an incorrect command e.g. `abcd` 20 | 21 | ## Adding a person: `add` 22 | Adds a person to the address book
23 | Format: `add NAME [p]p/PHONE_NUMBER [p]e/EMAIL [p]a/ADDRESS [t/TAG]...` 24 | 25 | > Words in `UPPER_CASE` are the parameters, items in `SQUARE_BRACKETS` are optional, 26 | > items with `...` after them can have multiple instances. Order of parameters are fixed. 27 | > 28 | > Put a `p` before the phone / email / address prefixes to mark it as `private`. `private` details can only 29 | > be seen using the `viewall` command. 30 | > 31 | > Persons can have any number of tags (including 0) 32 | 33 | Examples: 34 | * `add John Doe p/98765432 e/johnd@gmail.com a/John street, block 123, #01-01` 35 | * `add Betsy Crowe pp/1234567 e/betsycrowe@gmail.com pa/Newgate Prison t/criminal t/friend` 36 | 37 | ## Listing all persons : `list` 38 | Shows a list of all persons in the address book.
39 | Format: `list` 40 | 41 | ## Finding all persons containing any keyword in their name: `find` 42 | Finds persons whose names contain any of the given keywords.
43 | Format: `find KEYWORD [MORE_KEYWORDS]` 44 | 45 | > The search is case sensitive, the order of the keywords does not matter, only the name is searched, 46 | and persons matching at least one keyword will be returned (i.e. `OR` search). 47 | 48 | Examples: 49 | * `find John`
50 | Returns `John Doe` but not `john` 51 | * `find Betsy Tim John`
52 | Returns Any person having names `Betsy`, `Tim`, or `John` 53 | 54 | ## Deleting a person : `delete` 55 | Deletes the specified person from the address book. Irreversible.
56 | Format: `delete INDEX` 57 | 58 | > Deletes the person at the specified `INDEX`. 59 | The index refers to the index number shown in the most recent listing. 60 | 61 | Examples: 62 | * `list`
63 | `delete 2`
64 | Deletes the 2nd person in the address book. 65 | * `find Betsy`
66 | `delete 1`
67 | Deletes the 1st person in the results of the `find` command. 68 | 69 | ## View non-private details of a person : `view` 70 | Displays the non-private details of the specified person.
71 | Format: `view INDEX` 72 | 73 | > Views the person at the specified `INDEX`. 74 | The index refers to the index number shown in the most recent listing. 75 | 76 | Examples: 77 | * `list`
78 | `view 2`
79 | Views the 2nd person in the address book. 80 | * `find Betsy`
81 | `view 1`
82 | Views the 1st person in the results of the `find` command. 83 | 84 | ## View all details of a person : `viewall` 85 | Displays all details (including private details) of the specified person.
86 | Format: `viewall INDEX` 87 | 88 | > Views all details of the person at the specified `INDEX`. 89 | The index refers to the index number shown in the most recent listing. 90 | 91 | Examples: 92 | * `list`
93 | `viewall 2`
94 | Views all details of the 2nd person in the address book. 95 | * `find Betsy`
96 | `viewall 1`
97 | Views all details of the 1st person in the results of the `find` command. 98 | 99 | ## Clearing all entries : `clear` 100 | Clears all entries from the address book.
101 | Format: `clear` 102 | 103 | ## Exiting the program : `exit` 104 | Exits the program.
105 | Format: `exit` 106 | 107 | ## Saving the data 108 | Address book data are saved in the hard disk automatically after any command that changes the data.
109 | There is no need to save manually. Address book data are saved in a file called `addressbook.txt` in the project root folder. 110 | -------------------------------------------------------------------------------- /src/seedu/addressbook/storage/jaxb/AdaptedPerson.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.storage.jaxb; 2 | 3 | import seedu.addressbook.common.Utils; 4 | import seedu.addressbook.data.exception.IllegalValueException; 5 | import seedu.addressbook.data.person.*; 6 | import seedu.addressbook.data.tag.Tag; 7 | import seedu.addressbook.data.tag.UniqueTagList; 8 | 9 | import javax.xml.bind.annotation.XmlAttribute; 10 | import javax.xml.bind.annotation.XmlElement; 11 | import javax.xml.bind.annotation.XmlValue; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * JAXB-friendly adapted person data holder class. 17 | */ 18 | public class AdaptedPerson { 19 | 20 | private static class AdaptedContactDetail { 21 | @XmlValue 22 | public String value; 23 | @XmlAttribute(required = true) 24 | public boolean isPrivate; 25 | } 26 | 27 | @XmlElement(required = true) 28 | private String name; 29 | @XmlElement(required = true) 30 | private AdaptedContactDetail phone; 31 | @XmlElement(required = true) 32 | private AdaptedContactDetail email; 33 | @XmlElement(required = true) 34 | private AdaptedContactDetail address; 35 | 36 | @XmlElement 37 | private List tagged = new ArrayList<>(); 38 | 39 | /** 40 | * No-arg constructor for JAXB use. 41 | */ 42 | public AdaptedPerson() {} 43 | 44 | 45 | /** 46 | * Converts a given Person into this class for JAXB use. 47 | * 48 | * @param source future changes to this will not affect the created AdaptedPerson 49 | */ 50 | public AdaptedPerson(ReadOnlyPerson source) { 51 | name = source.getName().fullName; 52 | 53 | phone = new AdaptedContactDetail(); 54 | phone.isPrivate = source.getPhone().isPrivate(); 55 | phone.value = source.getPhone().value; 56 | 57 | email = new AdaptedContactDetail(); 58 | email.isPrivate = source.getEmail().isPrivate(); 59 | email.value = source.getEmail().value; 60 | 61 | address = new AdaptedContactDetail(); 62 | address.isPrivate = source.getAddress().isPrivate(); 63 | address.value = source.getAddress().value; 64 | 65 | tagged = new ArrayList<>(); 66 | for (Tag tag : source.getTags()) { 67 | tagged.add(new AdaptedTag(tag)); 68 | } 69 | } 70 | 71 | /** 72 | * Returns true if any required field is missing. 73 | * 74 | * JAXB does not enforce (required = true) without a given XML schema. 75 | * Since we do most of our validation using the data class constructors, the only extra logic we need 76 | * is to ensure that every xml element in the document is present. JAXB sets missing elements as null, 77 | * so we check for that. 78 | */ 79 | public boolean isAnyRequiredFieldMissing() { 80 | for (AdaptedTag tag : tagged) { 81 | if (tag.isAnyRequiredFieldMissing()) { 82 | return true; 83 | } 84 | } 85 | // second call only happens if phone/email/address are all not null 86 | return Utils.isAnyNull(name, phone, email, address) 87 | || Utils.isAnyNull(phone.value, email.value, address.value); 88 | } 89 | 90 | /** 91 | * Converts this jaxb-friendly adapted person object into the Person object. 92 | * 93 | * @throws IllegalValueException if there were any data constraints violated in the adapted person 94 | */ 95 | public Person toModelType() throws IllegalValueException { 96 | final List personTags = new ArrayList<>(); 97 | for (AdaptedTag tag : tagged) { 98 | personTags.add(tag.toModelType()); 99 | } 100 | final Name name = new Name(this.name); 101 | final Phone phone = new Phone(this.phone.value, this.phone.isPrivate); 102 | final Email email = new Email(this.email.value, this.email.isPrivate); 103 | final Address address = new Address(this.address.value, this.address.isPrivate); 104 | final UniqueTagList tags = new UniqueTagList(personTags); 105 | return new Person(name, phone, email, address, tags); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /doc/DeveloperGuide.md: -------------------------------------------------------------------------------- 1 | # Developer Guide 2 | 3 | * [Setting Up](#setting-up) 4 | * [Design](#design) 5 | * [Testing](#testing) 6 | * [Appendix A: User Stories](#appendix-a--user-stories) 7 | * [Appendix B: Use Cases](#appendix-b--use-cases) 8 | * [Appendix C: Non Functional Requirements](#appendix-c--non-functional-requirements) 9 | * [Appendix D: Gloassary](#appendix-d--glossary) 10 | 11 | ## Setting up 12 | 13 | #### Prerequisites 14 | 15 | * JDK 8 or later 16 | * IntelliJ IDE 17 | 18 | #### Importing the project into IntelliJ 19 | 20 | 1. Open IntelliJ (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project dialog first) 21 | 2. Set up the correct JDK version 22 | 1. Click `Configure` > `Project Defaults` > `Project Structure` 23 | 2. If JDK 8 is listed in the drop down, select it. If it is not, click `New...` and select the directory where you installed JDK 8. 24 | 3. Click `OK`. 25 | 3. Click `Import Project` 26 | 4. Locate the project directory and click `OK` 27 | 5. Select `Create project from existing sources` and click `Next` 28 | 6. Rename the project if you want. Click `Next` 29 | 7. Ensure that your `\src` and `\test\java` folder is checked. Keep clicking `Next` 30 | 8. Click `Finish` 31 | 9. Add JUnit 4 to classpath 32 | 1. Open any test file in `\test\java` and place your cursor over any `@Test` highlighted in red 33 | 2. Press ALT+ENTER and select `Add 'JUnit4' to classpath` 34 | 3. Select `Use 'JUnit4' from IntelliJ IDEA distribution` and click `OK` 35 | 10. Run all the tests (right-click the `test` folder, and click `Run 'All Tests'`) 36 | 11. Observe how some tests fail. That is because they try to access the test data from the wrong directory (the working directory is expected to be the root directory, but IntelliJ runs the test with `test\` as the working directory by default). To fix this issue: 37 | 1. Go to `Run` -> `Edit Configurations...` 38 | 2. On the list at the left, expand `JUnit`, and remove all the test configurations (e.g. `All in test`) by selecting it and clicking on the '-' icon at the top of the list 39 | 3. Expand `Defaults`, and ensure that `JUnit` is selected 40 | 4. Under `Configuration`, change the `Working directory` to the `addressbook-level3` folder 41 | 5. Click `OK` 42 | 12. Run the tests again to ensure they all pass now. 43 | 44 | ## Design 45 | 46 | 47 | ## Testing 48 | 49 | * In IntelliJ, right-click on the `test` folder and choose `Run 'All Tests'` 50 | 51 | ## Appendix A : User Stories 52 | 53 | Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` 54 | 55 | 56 | Priority | As a ... | I want to ... | So that I can... 57 | -------- | :-------- | :--------- | :----------- 58 | `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App 59 | `* * *` | user | add a new person | 60 | `* * *` | user | delete a person | remove entries that I no longer need 61 | `* * *` | user | find a person by name | locate details of persons without having to go through the entire list 62 | `* *` | user | hide [private contact details](#private-contact-detail) by default | minimize chance of someone else seeing them by accident 63 | `*` | user with many persons in the address book | sort persons by name | locate a person easily 64 | 65 | 66 | ## Appendix B : Use Cases 67 | 68 | (For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) 69 | 70 | #### Use case: Delete person 71 | 72 | **MSS** 73 | 74 | 1. User requests to list persons 75 | 2. AddressBook shows a list of persons 76 | 3. User requests to delete a specific person in the list 77 | 4. AddressBook deletes the person
78 | Use case ends. 79 | 80 | **Extensions** 81 | 82 | 2a. The list is empty 83 | 84 | > Use case ends 85 | 86 | 3a. The given index is invalid 87 | 88 | > 3a1. AddressBook shows an error message
89 | Use case resumes at step 2 90 | 91 | ## Appendix C : Non Functional Requirements 92 | 93 | 1. Should work on any [mainstream OS](#mainstream-os) as long as it has Java 8 or higher installed. 94 | 2. Should be able to hold up to 1000 persons. 95 | 3. Should come with automated unit tests and open source code. 96 | 4. Should favor DOS style commands over Unix-style commands. 97 | 98 | ## Appendix D : Glossary 99 | 100 | ##### Mainstream OS 101 | 102 | > Windows, Linux, Unix, OS-X 103 | 104 | ##### Private contact detail 105 | 106 | > A contact detail that is not meant to be shared with others 107 | -------------------------------------------------------------------------------- /test/java/seedu/addressbook/storage/StorageFileTest.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.storage; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import java.nio.file.Paths; 5 | import java.util.Collections; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.ExpectedException; 9 | import org.junit.rules.TemporaryFolder; 10 | 11 | import seedu.addressbook.data.AddressBook; 12 | import seedu.addressbook.data.exception.IllegalValueException; 13 | import seedu.addressbook.data.person.Address; 14 | import seedu.addressbook.data.person.Email; 15 | import seedu.addressbook.data.person.Name; 16 | import seedu.addressbook.data.person.Person; 17 | import seedu.addressbook.data.person.Phone; 18 | import seedu.addressbook.data.tag.Tag; 19 | import seedu.addressbook.data.tag.UniqueTagList; 20 | import seedu.addressbook.storage.StorageFile.StorageOperationException; 21 | import static seedu.addressbook.util.TestUtil.assertTextFilesEqual; 22 | 23 | public class StorageFileTest { 24 | private static final String TEST_DATA_FOLDER = "test/data/StorageFileTest"; 25 | 26 | @Rule 27 | public ExpectedException thrown = ExpectedException.none(); 28 | 29 | @Rule 30 | public TemporaryFolder testFolder = new TemporaryFolder(); 31 | 32 | @Test 33 | public void constructor_nullFilePath_exceptionThrown() throws Exception { 34 | thrown.expect(NullPointerException.class); 35 | new StorageFile(null); 36 | } 37 | 38 | @Test 39 | public void constructor_noTxtExtension_exceptionThrown() throws Exception { 40 | thrown.expect(IllegalValueException.class); 41 | new StorageFile(TEST_DATA_FOLDER + "/" + "InvalidfileName"); 42 | } 43 | 44 | @Test 45 | public void load_invalidFormat_exceptionThrown() throws Exception { 46 | // The file contains valid xml data, but does not match the AddressBook class 47 | StorageFile storage = getStorage("InvalidData.txt"); 48 | thrown.expect(StorageOperationException.class); 49 | storage.load(); 50 | } 51 | 52 | @Test 53 | public void load_validFormat() throws Exception { 54 | AddressBook actualAB = getStorage("ValidData.txt").load(); 55 | AddressBook expectedAB = getTestAddressBook(); 56 | 57 | // ensure loaded AddressBook is properly constructed with test data 58 | // TODO: overwrite equals method in AddressBook class and replace with equals method below 59 | assertEquals(actualAB.getAllPersons(), expectedAB.getAllPersons()); 60 | } 61 | 62 | @Test 63 | public void save_nullAddressBook_exceptionThrown() throws Exception { 64 | StorageFile storage = getTempStorage(); 65 | thrown.expect(NullPointerException.class); 66 | storage.save(null); 67 | } 68 | 69 | @Test 70 | public void save_validAddressBook() throws Exception { 71 | AddressBook ab = getTestAddressBook(); 72 | StorageFile storage = getTempStorage(); 73 | storage.save(ab); 74 | 75 | assertStorageFilesEqual(storage, getStorage("ValidData.txt")); 76 | } 77 | 78 | // getPath() method in StorageFile class is trivial so it is not tested 79 | 80 | /** 81 | * Asserts that the contents of two storage files are the same. 82 | */ 83 | private void assertStorageFilesEqual(StorageFile sf1, StorageFile sf2) throws Exception { 84 | assertTextFilesEqual(Paths.get(sf1.getPath()), Paths.get(sf2.getPath())); 85 | } 86 | 87 | private StorageFile getStorage(String fileName) throws Exception { 88 | return new StorageFile(TEST_DATA_FOLDER + "/" + fileName); 89 | } 90 | 91 | private StorageFile getTempStorage() throws Exception { 92 | return new StorageFile(testFolder.getRoot().getPath() + "/" + "temp.txt"); 93 | } 94 | 95 | private AddressBook getTestAddressBook() throws Exception { 96 | AddressBook ab = new AddressBook(); 97 | ab.addPerson(new Person(new Name("John Doe"), 98 | new Phone("98765432", false), 99 | new Email("johnd@gmail.com", false), 100 | new Address("John street, block 123, #01-01", false), 101 | new UniqueTagList(Collections.emptySet()))); 102 | ab.addPerson(new Person(new Name("Betsy Crowe"), 103 | new Phone("1234567", true), 104 | new Email("betsycrowe@gmail.com", false), 105 | new Address("Newgate Prison", true), 106 | new UniqueTagList(new Tag("friend"), new Tag("criminal")))); 107 | return ab; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/person/UniquePersonList.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.person; 2 | 3 | import seedu.addressbook.common.Utils; 4 | import seedu.addressbook.data.exception.DuplicateDataException; 5 | 6 | import java.util.*; 7 | 8 | /** 9 | * A list of persons. Does not allow null elements or duplicates. 10 | * 11 | * @see Person#equals(Object) 12 | * @see Utils#elementsAreUnique(Collection) 13 | */ 14 | public class UniquePersonList implements Iterable { 15 | 16 | /** 17 | * Signals that an operation would have violated the 'no duplicates' property of the list. 18 | */ 19 | public static class DuplicatePersonException extends DuplicateDataException { 20 | protected DuplicatePersonException() { 21 | super("Operation would result in duplicate persons"); 22 | } 23 | } 24 | 25 | /** 26 | * Signals that an operation targeting a specified person in the list would fail because 27 | * there is no such matching person in the list. 28 | */ 29 | public static class PersonNotFoundException extends Exception {} 30 | 31 | private final List internalList = new ArrayList<>(); 32 | 33 | /** 34 | * Constructs empty person list. 35 | */ 36 | public UniquePersonList() {} 37 | 38 | /** 39 | * Constructs a person list with the given persons. 40 | */ 41 | public UniquePersonList(Person... persons) throws DuplicatePersonException { 42 | final List initialTags = Arrays.asList(persons); 43 | if (!Utils.elementsAreUnique(initialTags)) { 44 | throw new DuplicatePersonException(); 45 | } 46 | internalList.addAll(initialTags); 47 | } 48 | 49 | /** 50 | * Constructs a list from the items in the given collection. 51 | * @param persons a collection of persons 52 | * @throws DuplicatePersonException if the {@code persons} contains duplicate persons 53 | */ 54 | public UniquePersonList(Collection persons) throws DuplicatePersonException { 55 | if (!Utils.elementsAreUnique(persons)) { 56 | throw new DuplicatePersonException(); 57 | } 58 | internalList.addAll(persons); 59 | } 60 | 61 | /** 62 | * Constructs a shallow copy of the list. 63 | */ 64 | public UniquePersonList(UniquePersonList source) { 65 | internalList.addAll(source.internalList); 66 | } 67 | 68 | /** 69 | * Unmodifiable java List view with elements cast as immutable {@link ReadOnlyPerson}s. 70 | * For use with other methods/libraries. 71 | * Any changes to the internal list/elements are immediately visible in the returned list. 72 | */ 73 | public List immutableListView() { 74 | return Collections.unmodifiableList(internalList); 75 | } 76 | 77 | 78 | /** 79 | * Checks if the list contains an equivalent person as the given argument. 80 | */ 81 | public boolean contains(ReadOnlyPerson toCheck) { 82 | return internalList.contains(toCheck); 83 | } 84 | 85 | /** 86 | * Adds a person to the list. 87 | * 88 | * @throws DuplicatePersonException if the person to add is a duplicate of an existing person in the list. 89 | */ 90 | public void add(Person toAdd) throws DuplicatePersonException { 91 | if (contains(toAdd)) { 92 | throw new DuplicatePersonException(); 93 | } 94 | internalList.add(toAdd); 95 | } 96 | 97 | /** 98 | * Removes the equivalent person from the list. 99 | * 100 | * @throws PersonNotFoundException if no such person could be found in the list. 101 | */ 102 | public void remove(ReadOnlyPerson toRemove) throws PersonNotFoundException { 103 | final boolean personFoundAndDeleted = internalList.remove(toRemove); 104 | if (!personFoundAndDeleted) { 105 | throw new PersonNotFoundException(); 106 | } 107 | } 108 | 109 | /** 110 | * Clears all persons in list. 111 | */ 112 | public void clear() { 113 | internalList.clear(); 114 | } 115 | 116 | @Override 117 | public Iterator iterator() { 118 | return internalList.iterator(); 119 | } 120 | 121 | @Override 122 | public boolean equals(Object other) { 123 | return other == this // short circuit if same object 124 | || (other instanceof UniquePersonList // instanceof handles nulls 125 | && this.internalList.equals( 126 | ((UniquePersonList) other).internalList)); 127 | } 128 | 129 | @Override 130 | public int hashCode() { 131 | return internalList.hashCode(); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/AddressBook.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data; 2 | 3 | import seedu.addressbook.data.person.*; 4 | import seedu.addressbook.data.person.UniquePersonList.*; 5 | import seedu.addressbook.data.tag.Tag; 6 | import seedu.addressbook.data.tag.UniqueTagList; 7 | import seedu.addressbook.data.tag.UniqueTagList.*; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * Represents the entire address book. Contains the data of the address book. 13 | * 14 | * Guarantees: 15 | * - Every tag found in every person will also be found in the tag list. 16 | * - The tags in each person point to tag objects in the master list. (== equality) 17 | */ 18 | public class AddressBook { 19 | 20 | private final UniquePersonList allPersons; 21 | private final UniqueTagList allTags; // can contain tags not attached to any person 22 | 23 | public static AddressBook empty() { 24 | return new AddressBook(); 25 | } 26 | 27 | /** 28 | * Creates an empty address book. 29 | */ 30 | public AddressBook() { 31 | allPersons = new UniquePersonList(); 32 | allTags = new UniqueTagList(); 33 | } 34 | 35 | /** 36 | * Constructs an address book with the given data. 37 | * Also updates the tag list with any missing tags found in any person. 38 | * 39 | * @param persons external changes to this will not affect this address book 40 | * @param tags external changes to this will not affect this address book 41 | */ 42 | public AddressBook(UniquePersonList persons, UniqueTagList tags) { 43 | this.allPersons = new UniquePersonList(persons); 44 | this.allTags = new UniqueTagList(tags); 45 | for (Person p : allPersons) { 46 | syncTagsWithMasterList(p); 47 | } 48 | } 49 | 50 | /** 51 | * Ensures that every tag in this person: 52 | * - exists in the master list {@link #allTags} 53 | * - points to a Tag object in the master list 54 | */ 55 | private void syncTagsWithMasterList(Person person) { 56 | final UniqueTagList personTags = person.getTags(); 57 | allTags.mergeFrom(personTags); 58 | 59 | // Create map with values = tag object references in the master list 60 | final Map masterTagObjects = new HashMap<>(); 61 | for (Tag tag : allTags) { 62 | masterTagObjects.put(tag, tag); 63 | } 64 | 65 | // Rebuild the list of person tags using references from the master list 66 | final Set commonTagReferences = new HashSet<>(); 67 | for (Tag tag : personTags) { 68 | commonTagReferences.add(masterTagObjects.get(tag)); 69 | } 70 | person.setTags(new UniqueTagList(commonTagReferences)); 71 | } 72 | 73 | /** 74 | * Adds a person to the address book. 75 | * Also checks the new person's tags and updates {@link #allTags} with any new tags found, 76 | * and updates the Tag objects in the person to point to those in {@link #allTags}. 77 | * 78 | * @throws DuplicatePersonException if an equivalent person already exists. 79 | */ 80 | public void addPerson(Person toAdd) throws DuplicatePersonException { 81 | syncTagsWithMasterList(toAdd); 82 | allPersons.add(toAdd); 83 | } 84 | 85 | /** 86 | * Checks if an equivalent person exists in the address book. 87 | */ 88 | public boolean containsPerson(ReadOnlyPerson key) { 89 | return allPersons.contains(key); 90 | } 91 | 92 | /** 93 | * Removes the equivalent person from the address book. 94 | * 95 | * @throws PersonNotFoundException if no such Person could be found. 96 | */ 97 | public void removePerson(ReadOnlyPerson toRemove) throws PersonNotFoundException { 98 | allPersons.remove(toRemove); 99 | } 100 | 101 | /** 102 | * Clears all persons and tags from the address book. 103 | */ 104 | public void clear() { 105 | allPersons.clear(); 106 | allTags.clear(); 107 | } 108 | 109 | /** 110 | * Defensively copied UniquePersonList of all persons in the address book at the time of the call. 111 | */ 112 | public UniquePersonList getAllPersons() { 113 | return new UniquePersonList(allPersons); 114 | } 115 | 116 | /** 117 | * Defensively copied UniqueTagList of all tags in the address book at the time of the call. 118 | */ 119 | public UniqueTagList getAllTags() { 120 | return new UniqueTagList(allTags); 121 | } 122 | 123 | @Override 124 | public boolean equals(Object other) { 125 | return other == this // short circuit if same object 126 | || (other instanceof AddressBook // instanceof handles nulls 127 | && this.allPersons.equals(((AddressBook) other).allPersons) 128 | && this.allTags.equals(((AddressBook) other).allTags)); 129 | } 130 | 131 | @Override 132 | public int hashCode() { 133 | // use this method for custom fields hashing instead of implementing your own 134 | return Objects.hash(allPersons, allTags); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/seedu/addressbook/data/tag/UniqueTagList.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.data.tag; 2 | 3 | import seedu.addressbook.common.Utils; 4 | import seedu.addressbook.data.exception.DuplicateDataException; 5 | 6 | import java.util.*; 7 | 8 | /** 9 | * A list of tags. Does not allow nulls or duplicates. 10 | * 11 | * @see Tag#equals(Object) 12 | * @see Utils#elementsAreUnique(Collection) 13 | */ 14 | public class UniqueTagList implements Iterable { 15 | 16 | /** 17 | * Signals that an operation would have violated the 'no duplicates' property of the list. 18 | */ 19 | public static class DuplicateTagException extends DuplicateDataException { 20 | protected DuplicateTagException() { 21 | super("Operation would result in duplicate tags"); 22 | } 23 | } 24 | 25 | /** 26 | * Signals that an operation targeting a specified Tag in the list would fail because 27 | * there is no such matching Tag in the list. 28 | */ 29 | public static class TagNotFoundException extends Exception {} 30 | 31 | private final List internalList = new ArrayList<>(); 32 | 33 | /** 34 | * Constructs an empty TagList. 35 | */ 36 | public UniqueTagList() {} 37 | 38 | /** 39 | * Constructs a tag list with the given tags. 40 | */ 41 | public UniqueTagList(Tag... tags) throws DuplicateTagException { 42 | final List initialTags = Arrays.asList(tags); 43 | if (!Utils.elementsAreUnique(initialTags)) { 44 | throw new DuplicateTagException(); 45 | } 46 | internalList.addAll(initialTags); 47 | } 48 | 49 | /** 50 | * Constructs a tag list with the given tags. 51 | */ 52 | public UniqueTagList(Collection tags) throws DuplicateTagException { 53 | if (!Utils.elementsAreUnique(tags)) { 54 | throw new DuplicateTagException(); 55 | } 56 | internalList.addAll(tags); 57 | } 58 | 59 | /** 60 | * Constructs a tag list with the given tags. 61 | */ 62 | public UniqueTagList(Set tags) { 63 | internalList.addAll(tags); 64 | } 65 | 66 | /** 67 | * Constructs a shallow copy of the given tag list. 68 | */ 69 | public UniqueTagList(UniqueTagList source) { 70 | internalList.addAll(source.internalList); 71 | } 72 | 73 | /** 74 | * All tags in this list as a Set. This set is mutable and change-insulated against the internal list. 75 | */ 76 | public Set toSet() { 77 | return new HashSet<>(internalList); 78 | } 79 | 80 | /** 81 | * Checks if the list contains an equivalent Tag as the given argument. 82 | */ 83 | public boolean contains(Tag toCheck) { 84 | return internalList.contains(toCheck); 85 | } 86 | 87 | /** 88 | * Adds a Tag to the list. 89 | * 90 | * @throws DuplicateTagException if the Tag to add is a duplicate of an existing Tag in the list. 91 | */ 92 | public void add(Tag toAdd) throws DuplicateTagException { 93 | if (contains(toAdd)) { 94 | throw new DuplicateTagException(); 95 | } 96 | internalList.add(toAdd); 97 | } 98 | 99 | /** 100 | * Adds all the given tags to this list. 101 | * 102 | * @throws DuplicateTagException if the argument tag list contains tag(s) that already exist in this list. 103 | */ 104 | public void addAll(UniqueTagList tags) throws DuplicateTagException { 105 | if (!Collections.disjoint(this.internalList, tags.internalList)) { 106 | throw new DuplicateTagException(); 107 | } 108 | this.internalList.addAll(tags.internalList); 109 | } 110 | 111 | /** 112 | * Adds every tag from the argument list that does not yet exist in this list. 113 | */ 114 | public void mergeFrom(UniqueTagList tags) { 115 | final Set alreadyInside = this.toSet(); 116 | for (Tag tag : tags) { 117 | if (!alreadyInside.contains(tag)) { 118 | internalList.add(tag); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Removes the equivalent Tag from the list. 125 | * 126 | * @throws TagNotFoundException if no such Tag could be found in the list. 127 | */ 128 | public void remove(Tag toRemove) throws TagNotFoundException { 129 | final boolean TagFoundAndDeleted = internalList.remove(toRemove); 130 | if (!TagFoundAndDeleted) { 131 | throw new TagNotFoundException(); 132 | } 133 | } 134 | 135 | /** 136 | * Clears all tags in list. 137 | */ 138 | public void clear() { 139 | internalList.clear(); 140 | } 141 | 142 | /** 143 | * Replaces the Tags in this list with those in the argument tag list. 144 | */ 145 | public void setTags(UniqueTagList replacement) { 146 | this.internalList.clear(); 147 | this.internalList.addAll(replacement.internalList); 148 | } 149 | 150 | @Override 151 | public Iterator iterator() { 152 | return internalList.iterator(); 153 | } 154 | 155 | @Override 156 | public boolean equals(Object other) { 157 | return other == this // short circuit if same object 158 | || (other instanceof UniqueTagList // instanceof handles nulls 159 | && this.internalList.equals( 160 | ((UniqueTagList) other).internalList)); 161 | } 162 | 163 | @Override 164 | public int hashCode() { 165 | return internalList.hashCode(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/seedu/addressbook/storage/StorageFile.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.storage; 2 | 3 | import seedu.addressbook.data.AddressBook; 4 | import seedu.addressbook.data.exception.IllegalValueException; 5 | import seedu.addressbook.storage.jaxb.AdaptedAddressBook; 6 | 7 | import javax.xml.bind.JAXBContext; 8 | import javax.xml.bind.JAXBException; 9 | import javax.xml.bind.Marshaller; 10 | import javax.xml.bind.Unmarshaller; 11 | import java.io.*; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | 15 | /** 16 | * Represents the file used to store address book data. 17 | */ 18 | public class StorageFile { 19 | 20 | /** Default file path used if the user doesn't provide the file name. */ 21 | public static final String DEFAULT_STORAGE_FILEPATH = "addressbook.txt"; 22 | 23 | /* Note: Note the use of nested classes below. 24 | * More info https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html 25 | */ 26 | 27 | /** 28 | * Signals that the given file path does not fulfill the storage filepath constraints. 29 | */ 30 | public static class InvalidStorageFilePathException extends IllegalValueException { 31 | public InvalidStorageFilePathException(String message) { 32 | super(message); 33 | } 34 | } 35 | 36 | /** 37 | * Signals that some error has occured while trying to convert and read/write data between the application 38 | * and the storage file. 39 | */ 40 | public static class StorageOperationException extends Exception { 41 | public StorageOperationException(String message) { 42 | super(message); 43 | } 44 | } 45 | 46 | private final JAXBContext jaxbContext; 47 | 48 | public final Path path; 49 | 50 | /** 51 | * @throws InvalidStorageFilePathException if the default path is invalid 52 | */ 53 | public StorageFile() throws InvalidStorageFilePathException { 54 | this(DEFAULT_STORAGE_FILEPATH); 55 | } 56 | 57 | /** 58 | * @throws InvalidStorageFilePathException if the given file path is invalid 59 | */ 60 | public StorageFile(String filePath) throws InvalidStorageFilePathException { 61 | try { 62 | jaxbContext = JAXBContext.newInstance(AdaptedAddressBook.class); 63 | } catch (JAXBException jaxbe) { 64 | throw new RuntimeException("jaxb initialisation error"); 65 | } 66 | 67 | path = Paths.get(filePath); 68 | if (!isValidPath(path)) { 69 | throw new InvalidStorageFilePathException("Storage file should end with '.txt'"); 70 | } 71 | } 72 | 73 | /** 74 | * Returns true if the given path is acceptable as a storage file. 75 | * The file path is considered acceptable if it ends with '.txt' 76 | */ 77 | private static boolean isValidPath(Path filePath) { 78 | return filePath.toString().endsWith(".txt"); 79 | } 80 | 81 | /** 82 | * Saves all data to this storage file. 83 | * 84 | * @throws StorageOperationException if there were errors converting and/or storing data to file. 85 | */ 86 | public void save(AddressBook addressBook) throws StorageOperationException { 87 | 88 | /* Note: Note the 'try with resource' statement below. 89 | * More info: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html 90 | */ 91 | try (final Writer fileWriter = 92 | new BufferedWriter(new FileWriter(path.toFile()))) { 93 | 94 | final AdaptedAddressBook toSave = new AdaptedAddressBook(addressBook); 95 | final Marshaller marshaller = jaxbContext.createMarshaller(); 96 | marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); 97 | marshaller.marshal(toSave, fileWriter); 98 | 99 | } catch (IOException ioe) { 100 | throw new StorageOperationException("Error writing to file: " + path + " error: " + ioe.getMessage()); 101 | } catch (JAXBException jaxbe) { 102 | throw new StorageOperationException("Error converting address book into storage format"); 103 | } 104 | } 105 | 106 | /** 107 | * Loads data from this storage file. 108 | * 109 | * @throws StorageOperationException if there were errors reading and/or converting data from file. 110 | */ 111 | public AddressBook load() throws StorageOperationException { 112 | try (final Reader fileReader = 113 | new BufferedReader(new FileReader(path.toFile()))) { 114 | 115 | final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); 116 | final AdaptedAddressBook loaded = (AdaptedAddressBook) unmarshaller.unmarshal(fileReader); 117 | // manual check for missing elements 118 | if (loaded.isAnyRequiredFieldMissing()) { 119 | throw new StorageOperationException("File data missing some elements"); 120 | } 121 | return loaded.toModelType(); 122 | 123 | /* Note: Here, we are using an exception to create the file if it is missing. However, we should minimize 124 | * using exceptions to facilitate normal paths of execution. If we consider the missing file as a 'normal' 125 | * situation (i.e. not truly exceptional) we should not use an exception to handle it. 126 | */ 127 | 128 | // create empty file if not found 129 | } catch (FileNotFoundException fnfe) { 130 | final AddressBook empty = new AddressBook(); 131 | save(empty); 132 | return empty; 133 | 134 | // other errors 135 | } catch (IOException ioe) { 136 | throw new StorageOperationException("Error writing to file: " + path); 137 | } catch (JAXBException jaxbe) { 138 | throw new StorageOperationException("Error parsing file data format"); 139 | } catch (IllegalValueException ive) { 140 | throw new StorageOperationException("File contains illegal data values; data type constraints not met"); 141 | } 142 | } 143 | 144 | public String getPath() { 145 | return path.toString(); 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/seedu/addressbook/parser/Parser.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.parser; 2 | 3 | import seedu.addressbook.commands.*; 4 | import seedu.addressbook.data.exception.IllegalValueException; 5 | 6 | import java.util.*; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import static seedu.addressbook.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; 11 | 12 | /** 13 | * Parses user input. 14 | */ 15 | public class Parser { 16 | 17 | public static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); 18 | 19 | public static final Pattern KEYWORDS_ARGS_FORMAT = 20 | Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace 21 | 22 | public static final Pattern PERSON_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes 23 | Pattern.compile("(?[^/]+)" 24 | + " (?p?)p/(?[^/]+)" 25 | + " (?p?)e/(?[^/]+)" 26 | + " (?p?)a/(?
[^/]+)" 27 | + "(?(?: t/[^/]+)*)"); // variable number of tags 28 | 29 | 30 | /** 31 | * Signals that the user input could not be parsed. 32 | */ 33 | public static class ParseException extends Exception { 34 | ParseException(String message) { 35 | super(message); 36 | } 37 | } 38 | 39 | /** 40 | * Used for initial separation of command word and args. 41 | */ 42 | public static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); 43 | 44 | /** 45 | * Parses user input into command for execution. 46 | * 47 | * @param userInput full user input string 48 | * @return the command based on the user input 49 | */ 50 | public Command parseCommand(String userInput) { 51 | final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); 52 | if (!matcher.matches()) { 53 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); 54 | } 55 | 56 | final String commandWord = matcher.group("commandWord"); 57 | final String arguments = matcher.group("arguments"); 58 | switch (commandWord) { 59 | 60 | case AddCommand.COMMAND_WORD: 61 | return prepareAdd(arguments); 62 | 63 | case DeleteCommand.COMMAND_WORD: 64 | return prepareDelete(arguments); 65 | 66 | case ClearCommand.COMMAND_WORD: 67 | return new ClearCommand(); 68 | 69 | case FindCommand.COMMAND_WORD: 70 | return prepareFind(arguments); 71 | 72 | case ListCommand.COMMAND_WORD: 73 | return new ListCommand(); 74 | 75 | case ViewCommand.COMMAND_WORD: 76 | return prepareView(arguments); 77 | 78 | case ViewAllCommand.COMMAND_WORD: 79 | return prepareViewAll(arguments); 80 | 81 | case ExitCommand.COMMAND_WORD: 82 | return new ExitCommand(); 83 | 84 | case HelpCommand.COMMAND_WORD: // Fallthrough 85 | default: 86 | return new HelpCommand(); 87 | } 88 | } 89 | 90 | /** 91 | * Parses arguments in the context of the add person command. 92 | * 93 | * @param args full command args string 94 | * @return the prepared command 95 | */ 96 | private Command prepareAdd(String args){ 97 | final Matcher matcher = PERSON_DATA_ARGS_FORMAT.matcher(args.trim()); 98 | // Validate arg string format 99 | if (!matcher.matches()) { 100 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); 101 | } 102 | try { 103 | return new AddCommand( 104 | matcher.group("name"), 105 | 106 | matcher.group("phone"), 107 | isPrivatePrefixPresent(matcher.group("isPhonePrivate")), 108 | 109 | matcher.group("email"), 110 | isPrivatePrefixPresent(matcher.group("isEmailPrivate")), 111 | 112 | matcher.group("address"), 113 | isPrivatePrefixPresent(matcher.group("isAddressPrivate")), 114 | 115 | getTagsFromArgs(matcher.group("tagArguments")) 116 | ); 117 | } catch (IllegalValueException ive) { 118 | return new IncorrectCommand(ive.getMessage()); 119 | } 120 | } 121 | 122 | /** 123 | * Checks whether the private prefix of a contact detail in the add command's arguments string is present. 124 | */ 125 | private static boolean isPrivatePrefixPresent(String matchedPrefix) { 126 | return matchedPrefix.equals("p"); 127 | } 128 | 129 | /** 130 | * Extracts the new person's tags from the add command's tag arguments string. 131 | * Merges duplicate tag strings. 132 | */ 133 | private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException { 134 | // no tags 135 | if (tagArguments.isEmpty()) { 136 | return Collections.emptySet(); 137 | } 138 | // replace first delimiter prefix, then split 139 | final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/")); 140 | return new HashSet<>(tagStrings); 141 | } 142 | 143 | 144 | /** 145 | * Parses arguments in the context of the delete person command. 146 | * 147 | * @param args full command args string 148 | * @return the prepared command 149 | */ 150 | private Command prepareDelete(String args) { 151 | try { 152 | final int targetIndex = parseArgsAsDisplayedIndex(args); 153 | return new DeleteCommand(targetIndex); 154 | } catch (ParseException | NumberFormatException e) { 155 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); 156 | } 157 | } 158 | 159 | /** 160 | * Parses arguments in the context of the view command. 161 | * 162 | * @param args full command args string 163 | * @return the prepared command 164 | */ 165 | private Command prepareView(String args) { 166 | 167 | try { 168 | final int targetIndex = parseArgsAsDisplayedIndex(args); 169 | return new ViewCommand(targetIndex); 170 | } catch (ParseException | NumberFormatException e) { 171 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, 172 | ViewCommand.MESSAGE_USAGE)); 173 | } 174 | } 175 | 176 | /** 177 | * Parses arguments in the context of the view all command. 178 | * 179 | * @param args full command args string 180 | * @return the prepared command 181 | */ 182 | private Command prepareViewAll(String args) { 183 | 184 | try { 185 | final int targetIndex = parseArgsAsDisplayedIndex(args); 186 | return new ViewAllCommand(targetIndex); 187 | } catch (ParseException | NumberFormatException e) { 188 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, 189 | ViewAllCommand.MESSAGE_USAGE)); 190 | } 191 | } 192 | 193 | /** 194 | * Parses the given arguments string as a single index number. 195 | * 196 | * @param args arguments string to parse as index number 197 | * @return the parsed index number 198 | * @throws ParseException if no region of the args string could be found for the index 199 | * @throws NumberFormatException the args string region is not a valid number 200 | */ 201 | private int parseArgsAsDisplayedIndex(String args) throws ParseException, NumberFormatException { 202 | final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(args.trim()); 203 | if (!matcher.matches()) { 204 | throw new ParseException("Could not find index number to parse"); 205 | } 206 | return Integer.parseInt(matcher.group("targetIndex")); 207 | } 208 | 209 | 210 | /** 211 | * Parses arguments in the context of the find person command. 212 | * 213 | * @param args full command args string 214 | * @return the prepared command 215 | */ 216 | private Command prepareFind(String args) { 217 | final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim()); 218 | if (!matcher.matches()) { 219 | return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, 220 | FindCommand.MESSAGE_USAGE)); 221 | } 222 | 223 | // keywords delimited by whitespace 224 | final String[] keywords = matcher.group("keywords").split("\\s+"); 225 | final Set keywordSet = new HashSet<>(Arrays.asList(keywords)); 226 | return new FindCommand(keywordSet); 227 | } 228 | 229 | 230 | } -------------------------------------------------------------------------------- /test/java/seedu/addressbook/parser/ParserTest.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.parser; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import seedu.addressbook.commands.*; 6 | import seedu.addressbook.data.exception.IllegalValueException; 7 | import seedu.addressbook.data.tag.Tag; 8 | import seedu.addressbook.data.tag.UniqueTagList; 9 | import seedu.addressbook.data.person.*; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | import static org.junit.Assert.*; 16 | import static seedu.addressbook.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; 17 | 18 | public class ParserTest { 19 | 20 | private Parser parser; 21 | 22 | @Before 23 | public void setup() { 24 | parser = new Parser(); 25 | } 26 | 27 | @Test 28 | public void emptyInput_returnsIncorrect() { 29 | final String[] emptyInputs = { "", " ", "\n \n" }; 30 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE); 31 | parseAndAssertIncorrectWithMessage(resultMessage, emptyInputs); 32 | } 33 | 34 | @Test 35 | public void unknownCommandWord_returnsHelp() { 36 | final String input = "unknowncommandword arguments arguments"; 37 | parseAndAssertCommandType(input, HelpCommand.class); 38 | } 39 | 40 | /** 41 | * Test 0-argument commands 42 | */ 43 | 44 | @Test 45 | public void helpCommand_parsedCorrectly() { 46 | final String input = "help"; 47 | parseAndAssertCommandType(input, HelpCommand.class); 48 | } 49 | 50 | @Test 51 | public void clearCommand_parsedCorrectly() { 52 | final String input = "clear"; 53 | parseAndAssertCommandType(input, ClearCommand.class); 54 | } 55 | 56 | @Test 57 | public void listCommand_parsedCorrectly() { 58 | final String input = "list"; 59 | parseAndAssertCommandType(input, ListCommand.class); 60 | } 61 | 62 | @Test 63 | public void exitCommand_parsedCorrectly() { 64 | final String input = "exit"; 65 | parseAndAssertCommandType(input, ExitCommand.class); 66 | } 67 | 68 | /** 69 | * Test ingle index argument commands 70 | */ 71 | 72 | @Test 73 | public void deleteCommand_noArgs() { 74 | final String[] inputs = { "delete", "delete " }; 75 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); 76 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 77 | } 78 | 79 | @Test 80 | public void deleteCommand_argsIsNotSingleNumber() { 81 | final String[] inputs = { "delete notAnumber ", "delete 8*wh12", "delete 1 2 3 4 5" }; 82 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); 83 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 84 | } 85 | 86 | @Test 87 | public void deleteCommand_numericArg_indexParsedCorrectly() { 88 | final int testIndex = 1; 89 | final String input = "delete " + testIndex; 90 | final DeleteCommand result = parseAndAssertCommandType(input, DeleteCommand.class); 91 | assertEquals(result.getTargetIndex(), testIndex); 92 | } 93 | 94 | @Test 95 | public void viewCommand_noArgs() { 96 | final String[] inputs = { "view", "view " }; 97 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE); 98 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 99 | } 100 | 101 | @Test 102 | public void viewCommand_argsIsNotSingleNumber() { 103 | final String[] inputs = { "view notAnumber ", "view 8*wh12", "view 1 2 3 4 5" }; 104 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE); 105 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 106 | } 107 | 108 | @Test 109 | public void viewCommand_numericArg_indexParsedCorrectly() { 110 | final int testIndex = 2; 111 | final String input = "view " + testIndex; 112 | final ViewCommand result = parseAndAssertCommandType(input, ViewCommand.class); 113 | assertEquals(result.getTargetIndex(), testIndex); 114 | } 115 | 116 | @Test 117 | public void viewAllCommand_noArgs() { 118 | final String[] inputs = { "viewall", "viewall " }; 119 | final String resultMessage = 120 | String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAllCommand.MESSAGE_USAGE); 121 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 122 | } 123 | 124 | @Test 125 | public void viewAllCommand_argsIsNotSingleNumber() { 126 | final String[] inputs = { "viewall notAnumber ", "viewall 8*wh12", "viewall 1 2 3 4 5" }; 127 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAllCommand.MESSAGE_USAGE); 128 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 129 | } 130 | 131 | @Test 132 | public void viewAllCommand_numericArg_indexParsedCorrectly() { 133 | final int testIndex = 3; 134 | final String input = "viewall " + testIndex; 135 | final ViewAllCommand result = parseAndAssertCommandType(input, ViewAllCommand.class); 136 | assertEquals(result.getTargetIndex(), testIndex); 137 | } 138 | 139 | /** 140 | * Test find persons by keyword in name command 141 | */ 142 | 143 | @Test 144 | public void findCommand_invalidArgs() { 145 | // no keywords 146 | final String[] inputs = { 147 | "find", 148 | "find " 149 | }; 150 | final String resultMessage = 151 | String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); 152 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 153 | } 154 | 155 | @Test 156 | public void findCommand_validArgs_parsedCorrectly() { 157 | final String[] keywords = { "key1", "key2", "key3" }; 158 | final Set keySet = new HashSet<>(Arrays.asList(keywords)); 159 | 160 | final String input = "find " + String.join(" ", keySet); 161 | final FindCommand result = 162 | parseAndAssertCommandType(input, FindCommand.class); 163 | assertEquals(keySet, result.getKeywords()); 164 | } 165 | 166 | @Test 167 | public void findCommand_duplicateKeys_parsedCorrectly() { 168 | final String[] keywords = { "key1", "key2", "key3" }; 169 | final Set keySet = new HashSet<>(Arrays.asList(keywords)); 170 | 171 | // duplicate every keyword 172 | final String input = "find " + String.join(" ", keySet) + " " + String.join(" ", keySet); 173 | final FindCommand result = 174 | parseAndAssertCommandType(input, FindCommand.class); 175 | assertEquals(keySet, result.getKeywords()); 176 | } 177 | 178 | /** 179 | * Test add person command 180 | */ 181 | 182 | @Test 183 | public void addCommand_invalidArgs() { 184 | final String[] inputs = { 185 | "add", 186 | "add ", 187 | "add wrong args format", 188 | // no phone prefix 189 | String.format("add $s $s e/$s a/$s", Name.EXAMPLE, Phone.EXAMPLE, Email.EXAMPLE, Address.EXAMPLE), 190 | // no email prefix 191 | String.format("add $s p/$s $s a/$s", Name.EXAMPLE, Phone.EXAMPLE, Email.EXAMPLE, Address.EXAMPLE), 192 | // no address prefix 193 | String.format("add $s p/$s e/$s $s", Name.EXAMPLE, Phone.EXAMPLE, Email.EXAMPLE, Address.EXAMPLE) 194 | }; 195 | final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); 196 | parseAndAssertIncorrectWithMessage(resultMessage, inputs); 197 | } 198 | 199 | @Test 200 | public void addCommand_invalidPersonDataInArgs() { 201 | final String invalidName = "[]\\[;]"; 202 | final String validName = Name.EXAMPLE; 203 | final String invalidPhoneArg = "p/not__numbers"; 204 | final String validPhoneArg = "p/" + Phone.EXAMPLE; 205 | final String invalidEmailArg = "e/notAnEmail123"; 206 | final String validEmailArg = "e/" + Email.EXAMPLE; 207 | final String invalidTagArg = "t/invalid_-[.tag"; 208 | 209 | // address can be any string, so no invalid address 210 | final String addCommandFormatString = "add $s $s $s a/" + Address.EXAMPLE; 211 | 212 | // test each incorrect person data field argument individually 213 | final String[] inputs = { 214 | // invalid name 215 | String.format(addCommandFormatString, invalidName, validPhoneArg, validEmailArg), 216 | // invalid phone 217 | String.format(addCommandFormatString, validName, invalidPhoneArg, validEmailArg), 218 | // invalid email 219 | String.format(addCommandFormatString, validName, validPhoneArg, invalidEmailArg), 220 | // invalid tag 221 | String.format(addCommandFormatString, validName, validPhoneArg, validEmailArg) + " " + invalidTagArg 222 | }; 223 | for (String input : inputs) { 224 | parseAndAssertCommandType(input, IncorrectCommand.class); 225 | } 226 | } 227 | 228 | @Test 229 | public void addCommand_validPersonData_parsedCorrectly() { 230 | final Person testPerson = generateTestPerson(); 231 | final String input = convertPersonToAddCommandString(testPerson); 232 | final AddCommand result = parseAndAssertCommandType(input, AddCommand.class); 233 | assertEquals(result.getPerson(), testPerson); 234 | } 235 | 236 | @Test 237 | public void addCommand_duplicateTags_merged() throws IllegalValueException { 238 | final Person testPerson = generateTestPerson(); 239 | String input = convertPersonToAddCommandString(testPerson); 240 | for (Tag tag : testPerson.getTags()) { 241 | // create duplicates by doubling each tag 242 | input += " t/" + tag.tagName; 243 | } 244 | 245 | final AddCommand result = parseAndAssertCommandType(input, AddCommand.class); 246 | assertEquals(result.getPerson(), testPerson); 247 | } 248 | 249 | private static Person generateTestPerson() { 250 | try { 251 | return new Person( 252 | new Name(Name.EXAMPLE), 253 | new Phone(Phone.EXAMPLE, true), 254 | new Email(Email.EXAMPLE, false), 255 | new Address(Address.EXAMPLE, true), 256 | new UniqueTagList(new Tag("tag1"), new Tag("tag2"), new Tag("tag3")) 257 | ); 258 | } catch (IllegalValueException ive) { 259 | throw new RuntimeException("test person data should be valid by definition"); 260 | } 261 | } 262 | 263 | private static String convertPersonToAddCommandString(ReadOnlyPerson person) { 264 | String addCommand = "add " 265 | + person.getName().fullName 266 | + (person.getPhone().isPrivate() ? " pp/" : " p/") + person.getPhone().value 267 | + (person.getEmail().isPrivate() ? " pe/" : " e/") + person.getEmail().value 268 | + (person.getAddress().isPrivate() ? " pa/" : " a/") + person.getAddress().value; 269 | for (Tag tag : person.getTags()) { 270 | addCommand += " t/" + tag.tagName; 271 | } 272 | return addCommand; 273 | } 274 | 275 | /** 276 | * Utility methods 277 | */ 278 | 279 | /** 280 | * Asserts that parsing the given inputs will return IncorrectCommand with the given feedback message. 281 | */ 282 | private void parseAndAssertIncorrectWithMessage(String feedbackMessage, String... inputs) { 283 | for (String input : inputs) { 284 | final IncorrectCommand result = parseAndAssertCommandType(input, IncorrectCommand.class); 285 | assertEquals(result.feedbackToUser, feedbackMessage); 286 | } 287 | } 288 | 289 | /** 290 | * Utility method for parsing input and asserting the class/type of the returned command object. 291 | * 292 | * @param input to be parsed 293 | * @param expectedCommandClass expected class of returned command 294 | * @return the parsed command object 295 | */ 296 | private T parseAndAssertCommandType(String input, Class expectedCommandClass) { 297 | final Command result = parser.parseCommand(input); 298 | assertTrue(result.getClass().isAssignableFrom(expectedCommandClass)); 299 | return (T) result; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /doc/LearningOutcomes.md: -------------------------------------------------------------------------------- 1 | # Learning Outcomes 2 | After studying this code and completing the corresponding exercises, you should be able to, 3 | 4 | 1. [Utilize User Stories `[LO-UserStories]`](#utilize-user-stories-lo-userstories) 5 | 1. [Utilize use cases `[LO-UseCases]`](#utilize-use-cases-lo-usecases) 6 | 1. [Use Non Functional Requirements `[LO-NFR]`](#use-non-functional-requirements-lo-nfr) 7 | 1. [Use Polymorphism `[LO-Polymorphism]`](#use-polymorphism-lo-polymorphism) 8 | 1. [Use abstract classes/methods `[LO-Abstract]`](#use-abstract-classesmethods-lo-abstract) 9 | 1. [Use interfaces `[LO-Interfaces]`](#use-interfaces-lo-interfaces) 10 | 1. [Follow Liskov Substitution Principle `[LO-LSP]`](#follow-liskov-substitution-principle-lo-lsp) 11 | 1. [Use Java-FX for GUI programming `[LO-JavaFx]`](#use-java-fx-for-gui-programming-lo-javafx) 12 | 1. [Analyze Coupling and Cohesion of designs `[LO-CouplingCohesion]`](#analyze-coupling-and-cohesion-of-designs-lo-couplingcohesion) 13 | 1. [Apply Dependency Inversion Principle `[LO-DIP]`](#apply-dependency-inversion-principle-lo-dip) 14 | 1. [Use Dependency Injection `[LO-DI]`](#use-dependency-injection-lo-di) 15 | 1. [Apply Open-Closed Principle `[LO-OCP]`](#apply-open-closed-principle-lo-ocp) 16 | 1. [Work in a 3KLoC code base `[LO-3KLoC]`](#work-in-a-3kloc-code-base-lo-3kloc) 17 | 18 | ------------------------------------------------------------------------------------------------------ 19 | 20 | ## Utilize User Stories `[LO-UserStories]` 21 | 22 | #### References 23 | 24 | * [se-edu/se-book: Requirements: Specifying Requirements: User Stories](https://se-edu.github.io/se-book/specifyingRequirements/userStories/) 25 | 26 | #### Exercise: Add more user stories 27 | 28 | * Assume you are planing to expand the functionality of the AddressBook (but keep it as a CLI application). 29 | What other user stories do you think AddressBook should support? Add those user stories to the `DeveloperGuide.md`. 30 | 31 | ------------------------------------------------------------------------------------------------------ 32 | 33 | ## Utilize use cases `[LO-UseCases]` 34 | 35 | #### References 36 | 37 | * [se-edu/se-book: Requirements: Specifying Requirements: Use Cases](https://se-edu.github.io/se-book/specifyingRequirements/useCases/) 38 | 39 | #### Exercise: Add a 'Rename tag' use case 40 | * Add a use case to the `DeveloperGuide.md` to cover the case of *renaming of an existing tag*.
41 | e.g. rename the tag `friends` to `buddies` (i.e. all persons who had the `friends` tag will now have 42 | a `buddies` tag instead)
43 | Assume that AddressBook confirms the change with the user before carrying out the operation. 44 | 45 | ------------------------------------------------------------------------------------------------------ 46 | 47 | ## Use Non Functional Requirements `[LO-NFR]` 48 | 49 | #### References 50 | 51 | * [se-edu/se-book: Requirements: Non-Functional Requirements](https://se-edu.github.io/se-book/requirements/nonFunctionalRequirements/) 52 | 53 | #### Exercise: Add more NFRs 54 | 55 | * Add some more NFRs to the `DeveloperGuide.md` 56 | 57 | ------------------------------------------------------------------------------------------------------ 58 | 59 | ## Use Polymorphism `[LO-Polymorphism]` 60 | 61 | Note how the `Command::execute()` method shows polymorphic behavior. 62 | 63 | #### References 64 | 65 | * [se-edu/se-book: Implementation: OOP: Polymorphism](https://se-edu.github.io/se-book/oopImplementation/polymorphism/) 66 | 67 | #### Exercise: Add a polymorphic `isMutating` method 68 | 69 | * Add a method `boolean isMutating()` to the `Command` class. This method will return `true` for 70 | command types that mutate the data. e.g. `AddCommand` 71 | * Currently, AddressBook data are saved to the file after every command. 72 | Take advantage of the the new method you added to limit file saving to only for command types that mutate data.
73 | i.e. `add` command should always save the data while `list` command should never save data to the file. 74 | 75 | Note: There may be better ways to limit file saving to commands that mutate data. The above approach, while not 76 | optimal, will give you chance to implement a polymorphic behavior. 77 | 78 | ------------------------------------------------------------------------------------------------------ 79 | 80 | ## Use abstract classes/methods `[LO-Abstract]` 81 | 82 | #### References 83 | 84 | * [se-edu/se-book: Implementation: OOP: Abstract Classes](https://se-edu.github.io/se-book/oopImplementation/abstractClasses/) 85 | 86 | #### Exercise: Make `Command#execute()` method abstract 87 | 88 | * Make the `Command#execute()` method abstract (hint: refer to the comment given below the method) 89 | 90 | ------------------------------------------------------------------------------------------------------ 91 | 92 | ## Use interfaces `[LO-Interfaces]` 93 | 94 | Note how the `Person` class implements the `ReadOnlyPerson` interface so that clients who don't need write access to `Person` objects can access `Person` objects through the `ReadOnlyPerson` interface instead. 95 | 96 | 97 | #### References 98 | 99 | * [se-edu/se-book: Implementation: OOP: Abstract Interfaces](https://se-edu.github.io/se-book/oopImplementation/interfaces/) 100 | 101 | ##### Exercise: Add a `Printable` interface 102 | 103 | * Add a `Printable` interface as follows.
104 | 105 | * `Override` the `getPrintableString` in classes `Name`, `Phone`, `Email`, and `Address` so that each produces a printable string representation of the object. e.g. `Name: John Smith`, `Phone: 12349862` 106 | * Add the following method in a suitable place of some other class. Note how the method depends on the Interface. 107 | 108 | ```java 109 | /** 110 | * Returns a concatenated version of the printable strings of each object. 111 | */ 112 | String getPrintableString(Printable... printables){ 113 | ``` 114 | 115 | The above method can be used to get a printable string representing a bunch of person details. 116 | For example, you should be able to call that method like this: 117 | 118 | ```java 119 | //p is a Person object 120 | return getPrintableString(p.getPhone(), p.getEmail(), p.getAddress()); 121 | ``` 122 | 123 | ------------------------------------------------------------------------------------------------------ 124 | 125 | ## Follow Liskov Substitution Principle `[LO-LSP]` 126 | 127 | #### References 128 | 129 | * [se-edu/se-book: Principles: Liskov Substitution Principle](https://se-edu.github.io/se-book/principles/liskovSubstitutionPrinciple/) 130 | 131 | #### Exercise: Add an exception to an overridden method 132 | 133 | * Add a `throws Exception` clause to the `AddCommand::execute` method. Notice how Java compiler will not allow it, 134 | unless you add the same `throws` clause to the parent class method. This is because if a child class throws 135 | an exception that is not specified by the Parent's contract, the child class is no longer substitutable in place of 136 | the parent class. 137 | * Also note that while in the above example the compiler enforces LSP, there are other situations where it is up to 138 | the programmer to enforce it. For example, if the method in the parent class works for `null` input, the overridden 139 | method in the child class should not reject `null` inputs. This will not be enforced by the compiler. 140 | 141 | ------------------------------------------------------------------------------------------------------ 142 | 143 | ## Use Java-FX for GUI programming `[LO-JavaFx]` 144 | 145 | #### References 146 | 147 | * [se-edu/se-book: Tools: Java: JavaFX: Basic](https://se-edu.github.io/se-book/javaTools/javaFXBasic/) 148 | 149 | #### Exercise: Enhance GUI 150 | 151 | * Do some enhancements to the AddressBook GUI. e.g. add an application icon, change font size/style 152 | 153 | ------------------------------------------------------------------------------------------------------ 154 | 155 | ## Analyze Coupling and Cohesion of designs `[LO-CouplingCohesion]` 156 | 157 | * Notice how having a separate `Formattter` class (an application of the Single Responsibility Principle) improves the *cohesion* of the `MainWindow` class as well as the `Formatter` class. 158 | 159 | #### References 160 | 161 | * [se-edu/se-book: Design: Design Principles: Coupling](https://se-edu.github.io/se-book/designPrinciples/coupling/) 162 | * [se-edu/se-book: Design: Design Principles: Cohesion](https://se-edu.github.io/se-book/designPrinciples/cohesion/) 163 | 164 | #### Exercise: Identify places to reduce coupling and increase cohesion 165 | 166 | * Where else in the design coupling can be reduced further, or cohesion can be increased further? 167 | 168 | ------------------------------------------------------------------------------------------------------ 169 | 170 | ## Apply Dependency Inversion Principle `[LO-DIP]` 171 | 172 | #### References 173 | 174 | * [se-edu/se-book: Principles: Dependency Inversion Principle](https://se-edu.github.io/se-book/principles/dependencyInversionPrinciple/) 175 | 176 | #### Exercise: Invert dependency from Logic to Storage 177 | 178 | * Note how `Logic` class depends on the `StorageFile` class. This is a violation of DIP. 179 | * Modify the implementation as follows so that both `Logic` and `StorageFile` now depend on the 180 | `abstract` class `Storage`.
181 | 182 | * Where else in the code do you notice the application of DIP? 183 | 184 | ------------------------------------------------------------------------------------------------------ 185 | 186 | ## Use Dependency Injection `[LO-DI]` 187 | 188 | Note how `Logic` class depends on the `StorageFile` class. This means when testing the `Logic` class, 189 | our test cases execute the `StorageFile` class as well. What if we want to test the `Logic` class without 190 | getting the `StorageFile` class involved? That is a situation where we can use *Dependency Injection*. 191 | 192 | #### References 193 | 194 | * [se-edu/se-book: Quality Assurance: Testing: Dependency Injection](https://se-edu.github.io/se-book/testing/dependencyInjection/) 195 | 196 | #### Exercise: Facilitate injecting a StorageStub 197 | 198 | * Change the implementation as follows so that we can inject a `StorageStub` when testing the `Logic` 199 | class.
200 | 201 | 202 | > If you did the exercise in [`LO-DIP`](#apply-dependency-inversion-principle-lo-dip) 203 | already but those changes are in a different branch, you may be able to reuse some of those commits 204 | by cherry picking them from that branch to the branch you created for this exercise.
205 | Note: *cherry picking* is simply copy-pasting a commit from one branch to another. In SourceTree, you can 206 | right-click on the commit your want to copy to the current branch, and choose 'Cherry pick' 207 | * Implement the `StorageStub` such that calls to the `save` method do nothing (i.e. empty method body). 208 | * Update the `LogicTest` to work with the `StorageStub` instead of the actual `StorageFile` object.
209 | i.e. `Logic` injects a `StorageStub` object to replace the dependency of `Logic` on `StorageFile` before 210 | testing `Logic`. 211 | * The example above uses [DIP](#apply-dependency-inversion-principle-lo-dip) as a means to achieve DI. 212 | Note that there is another way to inject a `StorageStub` object, as shown below. 213 | In this case we do not apply the DIP but we still achieve DI.
214 | 215 | 216 | ------------------------------------------------------------------------------------------------------ 217 | 218 | ## Apply Open-Closed Principle `[LO-OCP]` 219 | 220 | #### References 221 | * [se-edu/se-book: Design: Desing Principles: Open-Closed Principle](https://se-edu.github.io/se-book/designPrinciples/openClosedPrinciple/) 222 | 223 | #### Exercise: Analyze OCP-compliance of the `Logic` class 224 | 225 | * Consider adding a new command to the Address Book. e.g. an `edit` command. Notice how little you need to change in the `Logic` class to extend its behavior so that it can execute the new command. 226 | That is because `Logic` follows the OCP i.e. `Logic` is *open to be extended* with more commands but *closed for modifications*. 227 | * Is it possible to make the `Parser` class more OCP-compliant in terms of extending it to handle more 228 | command types? 229 | * In terms of how it saves data, does `Logic` become more OCP-compliant 230 | after applying DIP as given in [`LO-DIP`](#apply-dependency-inversion-principle-lo-dip)? 231 | How can you improve `Logic`'s OCP-compliance further so that it can not only work with different types 232 | of storages, but different number of storages (e.g. save to both a text file and a database). 233 | 234 | ------------------------------------------------------------------------------------------------------ 235 | 236 | ## Work in a 3KLoC code base `[LO-3KLoC]` 237 | 238 | #### Exercise: Enhance AddressBook 239 | 240 | * Enhance AddressBook in some way. e.g. add a new command 241 | 242 | ------------------------------------------------------------------------------------------------------ 243 | 244 | -------------------------------------------------------------------------------- /test/java/seedu/addressbook/logic/LogicTest.java: -------------------------------------------------------------------------------- 1 | package seedu.addressbook.logic; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.rules.TemporaryFolder; 8 | import seedu.addressbook.commands.CommandResult; 9 | import seedu.addressbook.commands.*; 10 | import seedu.addressbook.common.Messages; 11 | import seedu.addressbook.data.AddressBook; 12 | import seedu.addressbook.data.person.*; 13 | import seedu.addressbook.data.tag.Tag; 14 | import seedu.addressbook.data.tag.UniqueTagList; 15 | import seedu.addressbook.storage.StorageFile; 16 | 17 | import java.util.*; 18 | 19 | import static junit.framework.TestCase.assertEquals; 20 | import static seedu.addressbook.common.Messages.*; 21 | 22 | 23 | public class LogicTest { 24 | 25 | /** 26 | * See https://github.com/junit-team/junit4/wiki/rules#temporaryfolder-rule 27 | */ 28 | @Rule 29 | public TemporaryFolder saveFolder = new TemporaryFolder(); 30 | 31 | private StorageFile saveFile; 32 | private AddressBook addressBook; 33 | private Logic logic; 34 | 35 | @Before 36 | public void setup() throws Exception { 37 | saveFile = new StorageFile(saveFolder.newFile("testSaveFile.txt").getPath()); 38 | addressBook = new AddressBook(); 39 | saveFile.save(addressBook); 40 | logic = new Logic(saveFile, addressBook); 41 | } 42 | 43 | @Test 44 | public void constructor() { 45 | //Constructor is called in the setup() method which executes before every test, no need to call it here again. 46 | 47 | //Confirm the last shown list is empty 48 | assertEquals(Collections.emptyList(), logic.getLastShownList()); 49 | } 50 | 51 | @Test 52 | public void execute_invalid() throws Exception { 53 | String invalidCommand = " "; 54 | assertCommandBehavior(invalidCommand, 55 | String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); 56 | } 57 | 58 | /** 59 | * Executes the command and confirms that the result message is correct. 60 | * Both the 'address book' and the 'last shown list' are expected to be empty. 61 | * @see #assertCommandBehavior(String, String, AddressBook, boolean, List) 62 | */ 63 | private void assertCommandBehavior(String inputCommand, String expectedMessage) throws Exception { 64 | assertCommandBehavior(inputCommand, expectedMessage, AddressBook.empty(),false, Collections.emptyList()); 65 | } 66 | 67 | /** 68 | * Executes the command and confirms that the result message is correct and 69 | * also confirms that the following three parts of the Logic object's state are as expected:
70 | * - the internal address book data are same as those in the {@code expectedAddressBook}
71 | * - the internal 'last shown list' matches the {@code expectedLastList}
72 | * - the storage file content matches data in {@code expectedAddressBook}
73 | */ 74 | private void assertCommandBehavior(String inputCommand, 75 | String expectedMessage, 76 | AddressBook expectedAddressBook, 77 | boolean isRelevantPersonsExpected, 78 | List lastShownList) throws Exception { 79 | 80 | //Execute the command 81 | CommandResult r = logic.execute(inputCommand); 82 | 83 | //Confirm the result contains the right data 84 | assertEquals(expectedMessage, r.feedbackToUser); 85 | assertEquals(r.getRelevantPersons().isPresent(), isRelevantPersonsExpected); 86 | if(isRelevantPersonsExpected){ 87 | assertEquals(lastShownList, r.getRelevantPersons().get()); 88 | } 89 | 90 | //Confirm the state of data is as expected 91 | assertEquals(expectedAddressBook, addressBook); 92 | assertEquals(lastShownList, logic.getLastShownList()); 93 | assertEquals(addressBook, saveFile.load()); 94 | } 95 | 96 | 97 | @Test 98 | public void execute_unknownCommandWord() throws Exception { 99 | String unknownCommand = "uicfhmowqewca"; 100 | assertCommandBehavior(unknownCommand, HelpCommand.MESSAGE_ALL_USAGES); 101 | } 102 | 103 | @Test 104 | public void execute_help() throws Exception { 105 | assertCommandBehavior("help", HelpCommand.MESSAGE_ALL_USAGES); 106 | } 107 | 108 | @Test 109 | public void execute_exit() throws Exception { 110 | assertCommandBehavior("exit", ExitCommand.MESSAGE_EXIT_ACKNOWEDGEMENT); 111 | } 112 | 113 | @Test 114 | public void execute_clear() throws Exception { 115 | TestDataHelper helper = new TestDataHelper(); 116 | addressBook.addPerson(helper.generatePerson(1, true)); 117 | addressBook.addPerson(helper.generatePerson(2, true)); 118 | addressBook.addPerson(helper.generatePerson(3, true)); 119 | 120 | assertCommandBehavior("clear", ClearCommand.MESSAGE_SUCCESS, AddressBook.empty(), false, Collections.emptyList()); 121 | } 122 | 123 | @Test 124 | public void execute_add_invalidArgsFormat() throws Exception { 125 | String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); 126 | assertCommandBehavior( 127 | "add wrong args wrong args", expectedMessage); 128 | assertCommandBehavior( 129 | "add Valid Name 12345 e/valid@email.butNoPhonePrefix a/valid, address", expectedMessage); 130 | assertCommandBehavior( 131 | "add Valid Name p/12345 valid@email.butNoPrefix a/valid, address", expectedMessage); 132 | assertCommandBehavior( 133 | "add Valid Name p/12345 e/valid@email.butNoAddressPrefix valid, address", expectedMessage); 134 | } 135 | 136 | @Test 137 | public void execute_add_invalidPersonData() throws Exception { 138 | assertCommandBehavior( 139 | "add []\\[;] p/12345 e/valid@e.mail a/valid, address", Name.MESSAGE_NAME_CONSTRAINTS); 140 | assertCommandBehavior( 141 | "add Valid Name p/not_numbers e/valid@e.mail a/valid, address", Phone.MESSAGE_PHONE_CONSTRAINTS); 142 | assertCommandBehavior( 143 | "add Valid Name p/12345 e/notAnEmail a/valid, address", Email.MESSAGE_EMAIL_CONSTRAINTS); 144 | assertCommandBehavior( 145 | "add Valid Name p/12345 e/valid@e.mail a/valid, address t/invalid_-[.tag", Tag.MESSAGE_TAG_CONSTRAINTS); 146 | 147 | } 148 | 149 | @Test 150 | public void execute_add_successful() throws Exception { 151 | // setup expectations 152 | TestDataHelper helper = new TestDataHelper(); 153 | Person toBeAdded = helper.adam(); 154 | AddressBook expectedAB = new AddressBook(); 155 | expectedAB.addPerson(toBeAdded); 156 | 157 | // execute command and verify result 158 | assertCommandBehavior(helper.generateAddCommand(toBeAdded), 159 | String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), 160 | expectedAB, 161 | false, 162 | Collections.emptyList()); 163 | 164 | } 165 | 166 | @Test 167 | public void execute_addDuplicate_notAllowed() throws Exception { 168 | // setup expectations 169 | TestDataHelper helper = new TestDataHelper(); 170 | Person toBeAdded = helper.adam(); 171 | AddressBook expectedAB = new AddressBook(); 172 | expectedAB.addPerson(toBeAdded); 173 | 174 | // setup starting state 175 | addressBook.addPerson(toBeAdded); // person already in internal address book 176 | 177 | // execute command and verify result 178 | assertCommandBehavior( 179 | helper.generateAddCommand(toBeAdded), 180 | AddCommand.MESSAGE_DUPLICATE_PERSON, 181 | expectedAB, 182 | false, 183 | Collections.emptyList()); 184 | 185 | } 186 | 187 | @Test 188 | public void execute_list_showsAllPersons() throws Exception { 189 | // prepare expectations 190 | TestDataHelper helper = new TestDataHelper(); 191 | AddressBook expectedAB = helper.generateAddressBook(false, true); 192 | List expectedList = expectedAB.getAllPersons().immutableListView(); 193 | 194 | // prepare address book state 195 | helper.addToAddressBook(addressBook, false, true); 196 | 197 | assertCommandBehavior("list", 198 | Command.getMessageForPersonListShownSummary(expectedList), 199 | expectedAB, 200 | true, 201 | expectedList); 202 | } 203 | 204 | @Test 205 | public void execute_view_invalidArgsFormat() throws Exception { 206 | String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE); 207 | assertCommandBehavior("view ", expectedMessage); 208 | assertCommandBehavior("view arg not number", expectedMessage); 209 | } 210 | 211 | @Test 212 | public void execute_view_invalidIndex() throws Exception { 213 | assertInvalidIndexBehaviorForCommand("view"); 214 | } 215 | 216 | /** 217 | * Confirms the 'invalid argument index number behaviour' for the given command 218 | * targeting a single person in the last shown list, using visible index. 219 | * @param commandWord to test assuming it targets a single person in the last shown list based on visible index. 220 | */ 221 | private void assertInvalidIndexBehaviorForCommand(String commandWord) throws Exception { 222 | String expectedMessage = Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; 223 | TestDataHelper helper = new TestDataHelper(); 224 | List lastShownList = helper.generatePersonList(false, true); 225 | 226 | logic.setLastShownList(lastShownList); 227 | 228 | assertCommandBehavior(commandWord + " -1", expectedMessage, AddressBook.empty(), false, lastShownList); 229 | assertCommandBehavior(commandWord + " 0", expectedMessage, AddressBook.empty(), false, lastShownList); 230 | assertCommandBehavior(commandWord + " 3", expectedMessage, AddressBook.empty(), false, lastShownList); 231 | 232 | } 233 | 234 | @Test 235 | public void execute_view_onlyShowsNonPrivate() throws Exception { 236 | 237 | TestDataHelper helper = new TestDataHelper(); 238 | Person p1 = helper.generatePerson(1, true); 239 | Person p2 = helper.generatePerson(2, false); 240 | List lastShownList = helper.generatePersonList(p1, p2); 241 | AddressBook expectedAB = helper.generateAddressBook(lastShownList); 242 | helper.addToAddressBook(addressBook, lastShownList); 243 | 244 | logic.setLastShownList(lastShownList); 245 | 246 | assertCommandBehavior("view 1", 247 | String.format(ViewCommand.MESSAGE_VIEW_PERSON_DETAILS, p1.getAsTextHidePrivate()), 248 | expectedAB, 249 | false, 250 | lastShownList); 251 | 252 | assertCommandBehavior("view 2", 253 | String.format(ViewCommand.MESSAGE_VIEW_PERSON_DETAILS, p2.getAsTextHidePrivate()), 254 | expectedAB, 255 | false, 256 | lastShownList); 257 | } 258 | 259 | @Test 260 | public void execute_tryToViewMissingPerson_errorMessage() throws Exception { 261 | TestDataHelper helper = new TestDataHelper(); 262 | Person p1 = helper.generatePerson(1, false); 263 | Person p2 = helper.generatePerson(2, false); 264 | List lastShownList = helper.generatePersonList(p1, p2); 265 | 266 | AddressBook expectedAB = new AddressBook(); 267 | expectedAB.addPerson(p2); 268 | 269 | addressBook.addPerson(p2); 270 | logic.setLastShownList(lastShownList); 271 | 272 | assertCommandBehavior("view 1", 273 | Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK, 274 | expectedAB, 275 | false, 276 | lastShownList); 277 | } 278 | 279 | @Test 280 | public void execute_viewAll_invalidArgsFormat() throws Exception { 281 | String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAllCommand.MESSAGE_USAGE); 282 | assertCommandBehavior("viewall ", expectedMessage); 283 | assertCommandBehavior("viewall arg not number", expectedMessage); 284 | } 285 | 286 | @Test 287 | public void execute_viewAll_invalidIndex() throws Exception { 288 | assertInvalidIndexBehaviorForCommand("viewall"); 289 | } 290 | 291 | @Test 292 | public void execute_viewAll_alsoShowsPrivate() throws Exception { 293 | TestDataHelper helper = new TestDataHelper(); 294 | Person p1 = helper.generatePerson(1, true); 295 | Person p2 = helper.generatePerson(2, false); 296 | List lastShownList = helper.generatePersonList(p1, p2); 297 | AddressBook expectedAB = helper.generateAddressBook(lastShownList); 298 | helper.addToAddressBook(addressBook, lastShownList); 299 | 300 | logic.setLastShownList(lastShownList); 301 | 302 | assertCommandBehavior("viewall 1", 303 | String.format(ViewCommand.MESSAGE_VIEW_PERSON_DETAILS, p1.getAsTextShowAll()), 304 | expectedAB, 305 | false, 306 | lastShownList); 307 | 308 | assertCommandBehavior("viewall 2", 309 | String.format(ViewCommand.MESSAGE_VIEW_PERSON_DETAILS, p2.getAsTextShowAll()), 310 | expectedAB, 311 | false, 312 | lastShownList); 313 | } 314 | 315 | @Test 316 | public void execute_tryToViewAllPersonMissingInAddressBook_errorMessage() throws Exception { 317 | TestDataHelper helper = new TestDataHelper(); 318 | Person p1 = helper.generatePerson(1, false); 319 | Person p2 = helper.generatePerson(2, false); 320 | List lastShownList = helper.generatePersonList(p1, p2); 321 | 322 | AddressBook expectedAB = new AddressBook(); 323 | expectedAB.addPerson(p1); 324 | 325 | addressBook.addPerson(p1); 326 | logic.setLastShownList(lastShownList); 327 | 328 | assertCommandBehavior("viewall 2", 329 | Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK, 330 | expectedAB, 331 | false, 332 | lastShownList); 333 | } 334 | 335 | @Test 336 | public void execute_delete_invalidArgsFormat() throws Exception { 337 | String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE); 338 | assertCommandBehavior("delete ", expectedMessage); 339 | assertCommandBehavior("delete arg not number", expectedMessage); 340 | } 341 | 342 | @Test 343 | public void execute_delete_invalidIndex() throws Exception { 344 | assertInvalidIndexBehaviorForCommand("delete"); 345 | } 346 | 347 | @Test 348 | public void execute_delete_removesCorrectPerson() throws Exception { 349 | TestDataHelper helper = new TestDataHelper(); 350 | Person p1 = helper.generatePerson(1, false); 351 | Person p2 = helper.generatePerson(2, true); 352 | Person p3 = helper.generatePerson(3, true); 353 | 354 | List threePersons = helper.generatePersonList(p1, p2, p3); 355 | 356 | AddressBook expectedAB = helper.generateAddressBook(threePersons); 357 | expectedAB.removePerson(p2); 358 | 359 | 360 | helper.addToAddressBook(addressBook, threePersons); 361 | logic.setLastShownList(threePersons); 362 | 363 | assertCommandBehavior("delete 2", 364 | String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, p2), 365 | expectedAB, 366 | false, 367 | threePersons); 368 | } 369 | 370 | @Test 371 | public void execute_delete_missingInAddressBook() throws Exception { 372 | 373 | TestDataHelper helper = new TestDataHelper(); 374 | Person p1 = helper.generatePerson(1, false); 375 | Person p2 = helper.generatePerson(2, true); 376 | Person p3 = helper.generatePerson(3, true); 377 | 378 | List threePersons = helper.generatePersonList(p1, p2, p3); 379 | 380 | AddressBook expectedAB = helper.generateAddressBook(threePersons); 381 | expectedAB.removePerson(p2); 382 | 383 | helper.addToAddressBook(addressBook, threePersons); 384 | addressBook.removePerson(p2); 385 | logic.setLastShownList(threePersons); 386 | 387 | assertCommandBehavior("delete 2", 388 | Messages.MESSAGE_PERSON_NOT_IN_ADDRESSBOOK, 389 | expectedAB, 390 | false, 391 | threePersons); 392 | } 393 | 394 | @Test 395 | public void execute_find_invalidArgsFormat() throws Exception { 396 | String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE); 397 | assertCommandBehavior("find ", expectedMessage); 398 | } 399 | 400 | @Test 401 | public void execute_find_onlyMatchesFullWordsInNames() throws Exception { 402 | TestDataHelper helper = new TestDataHelper(); 403 | Person pTarget1 = helper.generatePersonWithName("bla bla KEY bla"); 404 | Person pTarget2 = helper.generatePersonWithName("bla KEY bla bceofeia"); 405 | Person p1 = helper.generatePersonWithName("KE Y"); 406 | Person p2 = helper.generatePersonWithName("KEYKEYKEY sduauo"); 407 | 408 | List fourPersons = helper.generatePersonList(p1, pTarget1, p2, pTarget2); 409 | AddressBook expectedAB = helper.generateAddressBook(fourPersons); 410 | List expectedList = helper.generatePersonList(pTarget1, pTarget2); 411 | helper.addToAddressBook(addressBook, fourPersons); 412 | 413 | assertCommandBehavior("find KEY", 414 | Command.getMessageForPersonListShownSummary(expectedList), 415 | expectedAB, 416 | true, 417 | expectedList); 418 | } 419 | 420 | @Test 421 | public void execute_find_isCaseSensitive() throws Exception { 422 | TestDataHelper helper = new TestDataHelper(); 423 | Person pTarget1 = helper.generatePersonWithName("bla bla KEY bla"); 424 | Person pTarget2 = helper.generatePersonWithName("bla KEY bla bceofeia"); 425 | Person p1 = helper.generatePersonWithName("key key"); 426 | Person p2 = helper.generatePersonWithName("KEy sduauo"); 427 | 428 | List fourPersons = helper.generatePersonList(p1, pTarget1, p2, pTarget2); 429 | AddressBook expectedAB = helper.generateAddressBook(fourPersons); 430 | List expectedList = helper.generatePersonList(pTarget1, pTarget2); 431 | helper.addToAddressBook(addressBook, fourPersons); 432 | 433 | assertCommandBehavior("find KEY", 434 | Command.getMessageForPersonListShownSummary(expectedList), 435 | expectedAB, 436 | true, 437 | expectedList); 438 | } 439 | 440 | @Test 441 | public void execute_find_matchesIfAnyKeywordPresent() throws Exception { 442 | TestDataHelper helper = new TestDataHelper(); 443 | Person pTarget1 = helper.generatePersonWithName("bla bla KEY bla"); 444 | Person pTarget2 = helper.generatePersonWithName("bla rAnDoM bla bceofeia"); 445 | Person p1 = helper.generatePersonWithName("key key"); 446 | Person p2 = helper.generatePersonWithName("KEy sduauo"); 447 | 448 | List fourPersons = helper.generatePersonList(p1, pTarget1, p2, pTarget2); 449 | AddressBook expectedAB = helper.generateAddressBook(fourPersons); 450 | List expectedList = helper.generatePersonList(pTarget1, pTarget2); 451 | helper.addToAddressBook(addressBook, fourPersons); 452 | 453 | assertCommandBehavior("find KEY rAnDoM", 454 | Command.getMessageForPersonListShownSummary(expectedList), 455 | expectedAB, 456 | true, 457 | expectedList); 458 | } 459 | 460 | /** 461 | * A utility class to generate test data. 462 | */ 463 | class TestDataHelper{ 464 | 465 | Person adam() throws Exception { 466 | Name name = new Name("Adam Brown"); 467 | Phone privatePhone = new Phone("111111", true); 468 | Email email = new Email("adam@gmail.com", false); 469 | Address privateAddress = new Address("111, alpha street", true); 470 | Tag tag1 = new Tag("tag1"); 471 | Tag tag2 = new Tag("tag2"); 472 | UniqueTagList tags = new UniqueTagList(tag1, tag2); 473 | return new Person(name, privatePhone, email, privateAddress, tags); 474 | } 475 | 476 | /** 477 | * Generates a valid person using the given seed. 478 | * Running this function with the same parameter values guarantees the returned person will have the same state. 479 | * Each unique seed will generate a unique Person object. 480 | * 481 | * @param seed used to generate the person data field values 482 | * @param isAllFieldsPrivate determines if private-able fields (phone, email, address) will be private 483 | */ 484 | Person generatePerson(int seed, boolean isAllFieldsPrivate) throws Exception { 485 | return new Person( 486 | new Name("Person " + seed), 487 | new Phone("" + Math.abs(seed), isAllFieldsPrivate), 488 | new Email(seed + "@email", isAllFieldsPrivate), 489 | new Address("House of " + seed, isAllFieldsPrivate), 490 | new UniqueTagList(new Tag("tag" + Math.abs(seed)), new Tag("tag" + Math.abs(seed + 1))) 491 | ); 492 | } 493 | 494 | /** Generates the correct add command based on the person given */ 495 | String generateAddCommand(Person p) { 496 | StringJoiner cmd = new StringJoiner(" "); 497 | 498 | cmd.add("add"); 499 | 500 | cmd.add(p.getName().toString()); 501 | cmd.add((p.getPhone().isPrivate() ? "pp/" : "p/") + p.getPhone()); 502 | cmd.add((p.getEmail().isPrivate() ? "pe/" : "e/") + p.getEmail()); 503 | cmd.add((p.getAddress().isPrivate() ? "pa/" : "a/") + p.getAddress()); 504 | 505 | UniqueTagList tags = p.getTags(); 506 | for(Tag t: tags){ 507 | cmd.add("t/" + t.tagName); 508 | } 509 | 510 | return cmd.toString(); 511 | } 512 | 513 | /** 514 | * Generates an AddressBook with auto-generated persons. 515 | * @param isPrivateStatuses flags to indicate if all contact details of respective persons should be set to 516 | * private. 517 | */ 518 | AddressBook generateAddressBook(Boolean... isPrivateStatuses) throws Exception{ 519 | AddressBook addressBook = new AddressBook(); 520 | addToAddressBook(addressBook, isPrivateStatuses); 521 | return addressBook; 522 | } 523 | 524 | /** 525 | * Generates an AddressBook based on the list of Persons given. 526 | */ 527 | AddressBook generateAddressBook(List persons) throws Exception{ 528 | AddressBook addressBook = new AddressBook(); 529 | addToAddressBook(addressBook, persons); 530 | return addressBook; 531 | } 532 | 533 | /** 534 | * Adds auto-generated Person objects to the given AddressBook 535 | * @param addressBook The AddressBook to which the Persons will be added 536 | * @param isPrivateStatuses flags to indicate if all contact details of generated persons should be set to 537 | * private. 538 | */ 539 | void addToAddressBook(AddressBook addressBook, Boolean... isPrivateStatuses) throws Exception{ 540 | addToAddressBook(addressBook, generatePersonList(isPrivateStatuses)); 541 | } 542 | 543 | /** 544 | * Adds the given list of Persons to the given AddressBook 545 | */ 546 | void addToAddressBook(AddressBook addressBook, List personsToAdd) throws Exception{ 547 | for(Person p: personsToAdd){ 548 | addressBook.addPerson(p); 549 | } 550 | } 551 | 552 | /** 553 | * Creates a list of Persons based on the give Person objects. 554 | */ 555 | List generatePersonList(Person... persons) throws Exception{ 556 | List personList = new ArrayList<>(); 557 | for(Person p: persons){ 558 | personList.add(p); 559 | } 560 | return personList; 561 | } 562 | 563 | /** 564 | * Generates a list of Persons based on the flags. 565 | * @param isPrivateStatuses flags to indicate if all contact details of respective persons should be set to 566 | * private. 567 | */ 568 | List generatePersonList(Boolean... isPrivateStatuses) throws Exception{ 569 | List persons = new ArrayList<>(); 570 | int i = 1; 571 | for(Boolean p: isPrivateStatuses){ 572 | persons.add(generatePerson(i++, p)); 573 | } 574 | return persons; 575 | } 576 | 577 | /** 578 | * Generates a Person object with given name. Other fields will have some dummy values. 579 | */ 580 | Person generatePersonWithName(String name) throws Exception { 581 | return new Person( 582 | new Name(name), 583 | new Phone("1", false), 584 | new Email("1@email", false), 585 | new Address("House of 1", false), 586 | new UniqueTagList(new Tag("tag")) 587 | ); 588 | } 589 | } 590 | 591 | } 592 | --------------------------------------------------------------------------------