{
-
- protected LevelDeserializer(Class> vc) {
- super(vc);
- }
-
- @Override
- protected Level _deserialize(String value, DeserializationContext ctxt) throws IOException {
- return getLoggingLevel(value);
- }
-
- /**
- * Gets the logging level that matches loggingLevelString
- *
- * Returns null if there are no matches
- *
- * @param loggingLevelString
- * @return
- */
- private Level getLoggingLevel(String loggingLevelString) {
- return Level.parse(loggingLevelString);
- }
- @Override
- public Class handledType() {
- return Level.class;
- }
- }
+ private static final Logger logger = LogsCenter.getLogger(JsonUtil.class);
private static ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
@@ -56,6 +38,59 @@ public Class handledType() {
.addSerializer(Level.class, new ToStringSerializer())
.addDeserializer(Level.class, new LevelDeserializer(Level.class)));
+ static void serializeObjectToJsonFile(File jsonFile, T objectToSerialize) throws IOException {
+ FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize));
+ }
+
+ static T deserializeObjectFromJsonFile(File jsonFile, Class classOfObjectToDeserialize)
+ throws IOException {
+ return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize);
+ }
+
+ /**
+ * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found.
+ * If any values are missing from the file, default values will be used, as long as the file is a valid json file.
+ * @param filePath cannot be null.
+ * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here.
+ * @throws DataConversionException if the file format is not as expected.
+ */
+ public static Optional readJsonFile(
+ String filePath, Class classOfObjectToDeserialize) throws DataConversionException {
+ requireNonNull(filePath);
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ logger.info("Json file " + file + " not found");
+ return Optional.empty();
+ }
+
+ T jsonFile;
+
+ try {
+ jsonFile = deserializeObjectFromJsonFile(file, classOfObjectToDeserialize);
+ } catch (IOException e) {
+ logger.warning("Error reading from jsonFile file " + file + ": " + e);
+ throw new DataConversionException(e);
+ }
+
+ return Optional.of(jsonFile);
+ }
+
+ /**
+ * Saves the Json object to the specified file.
+ * Overwrites existing file if it exists, creates a new file if it doesn't.
+ * @param jsonFile cannot be null
+ * @param filePath cannot be null
+ * @throws IOException if there was an error during writing to the file
+ */
+ public static void saveJsonFile(T jsonFile, String filePath) throws IOException {
+ requireNonNull(filePath);
+ requireNonNull(jsonFile);
+
+ serializeObjectToJsonFile(new File(filePath), jsonFile);
+ }
+
+
/**
* Converts a given string representation of a JSON data to instance of a class
* @param The generic type to create an instance of
@@ -75,4 +110,34 @@ public static String toJsonString(T instance) throws JsonProcessingException
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance);
}
+ /**
+ * Contains methods that retrieve logging level from serialized string.
+ */
+ private static class LevelDeserializer extends FromStringDeserializer {
+
+ protected LevelDeserializer(Class> vc) {
+ super(vc);
+ }
+
+ @Override
+ protected Level _deserialize(String value, DeserializationContext ctxt) throws IOException {
+ return getLoggingLevel(value);
+ }
+
+ /**
+ * Gets the logging level that matches loggingLevelString
+ *
+ * Returns null if there are no matches
+ *
+ */
+ private Level getLoggingLevel(String loggingLevelString) {
+ return Level.parse(loggingLevelString);
+ }
+
+ @Override
+ public Class handledType() {
+ return Level.class;
+ }
+ }
+
}
diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java
index 2e94740456a6..6e403c17c96e 100644
--- a/src/main/java/seedu/address/commons/util/StringUtil.java
+++ b/src/main/java/seedu/address/commons/util/StringUtil.java
@@ -1,36 +1,71 @@
package seedu.address.commons.util;
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.AppUtil.checkArgument;
+
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.util.Arrays;
-import java.util.List;
/**
* Helper functions for handling strings.
*/
public class StringUtil {
- public static boolean containsIgnoreCase(String source, String query) {
- String[] split = source.toLowerCase().split("\\s+");
- List strings = Arrays.asList(split);
- return strings.stream().filter(s -> s.equals(query.toLowerCase())).count() > 0;
+
+ /**
+ * Returns true if the {@code sentence} contains the {@code word}.
+ * Ignores case, but a full word match is required.
+ *
examples:
+ * containsWordIgnoreCase("ABc def", "abc") == true
+ * containsWordIgnoreCase("ABc def", "DEF") == true
+ * containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
+ *
+ * @param sentence cannot be null
+ * @param word cannot be null, cannot be empty, must be a single word
+ */
+ public static boolean containsWordIgnoreCase(String sentence, String word) {
+ requireNonNull(sentence);
+ requireNonNull(word);
+
+ String preppedWord = word.trim();
+ checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty");
+ checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word");
+
+ String preppedSentence = sentence;
+ String[] wordsInPreppedSentence = preppedSentence.split("\\s+");
+
+ for (String wordInSentence: wordsInPreppedSentence) {
+ if (wordInSentence.equalsIgnoreCase(preppedWord)) {
+ return true;
+ }
+ }
+ return false;
}
/**
* Returns a detailed message of the t, including the stack trace.
*/
- public static String getDetails(Throwable t){
- assert t != null;
+ public static String getDetails(Throwable t) {
+ requireNonNull(t);
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
return t.getMessage() + "\n" + sw.toString();
}
/**
- * Returns true if s represents an unsigned integer e.g. 1, 2, 3, ...
- * Will return false for null, empty string, "-1", "0", "+1", and " 2 " (untrimmed) "3 0" (contains whitespace).
- * @param s Should be trimmed.
+ * Returns true if {@code s} represents a non-zero unsigned integer
+ * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
+ * Will return false for any other non-null string input
+ * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters)
+ * @throws NullPointerException if {@code s} is null.
*/
- public static boolean isUnsignedInteger(String s){
- return s != null && s.matches("^0*[1-9]\\d*$");
+ public static boolean isNonZeroUnsignedInteger(String s) {
+ requireNonNull(s);
+
+ try {
+ int value = Integer.parseInt(s);
+ return value > 0 && !s.startsWith("+"); // "+1" is successfully parsed by Integer#parseInt(String)
+ } catch (NumberFormatException nfe) {
+ return false;
+ }
}
}
diff --git a/src/main/java/seedu/address/commons/util/UrlUtil.java b/src/main/java/seedu/address/commons/util/UrlUtil.java
deleted file mode 100644
index 6bbab52b9840..000000000000
--- a/src/main/java/seedu/address/commons/util/UrlUtil.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package seedu.address.commons.util;
-
-import java.net.URL;
-
-/**
- * A utility class for URL
- */
-public class UrlUtil {
-
- /**
- * Returns true if both URLs have the same base URL
- */
- public static boolean compareBaseUrls(URL url1, URL url2) {
-
- if (url1 == null || url2 == null) {
- return false;
- }
- return url1.getHost().toLowerCase().replaceFirst("www.", "")
- .equals(url2.getHost().replaceFirst("www.", "").toLowerCase())
- && url1.getPath().replaceAll("/", "").toLowerCase()
- .equals(url2.getPath().replaceAll("/", "").toLowerCase());
- }
-
-}
diff --git a/src/main/java/seedu/address/commons/util/XmlUtil.java b/src/main/java/seedu/address/commons/util/XmlUtil.java
index 2087e7628a1d..5f61738627cc 100644
--- a/src/main/java/seedu/address/commons/util/XmlUtil.java
+++ b/src/main/java/seedu/address/commons/util/XmlUtil.java
@@ -1,11 +1,14 @@
package seedu.address.commons.util;
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
-import java.io.File;
-import java.io.FileNotFoundException;
/**
* Helps with reading from and writing to XML files.
@@ -26,8 +29,8 @@ public class XmlUtil {
public static T getDataFromFile(File file, Class classToConvert)
throws FileNotFoundException, JAXBException {
- assert file != null;
- assert classToConvert != null;
+ requireNonNull(file);
+ requireNonNull(classToConvert);
if (!FileUtil.isFileExists(file)) {
throw new FileNotFoundException("File not found : " + file.getAbsolutePath());
@@ -50,8 +53,8 @@ public static T getDataFromFile(File file, Class classToConvert)
*/
public static void saveDataToFile(File file, T data) throws FileNotFoundException, JAXBException {
- assert file != null;
- assert data != null;
+ requireNonNull(file);
+ requireNonNull(data);
if (!file.exists()) {
throw new FileNotFoundException("File not found : " + file.getAbsolutePath());
diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/seedu/address/logic/CommandHistory.java
new file mode 100644
index 000000000000..10821acb3e2d
--- /dev/null
+++ b/src/main/java/seedu/address/logic/CommandHistory.java
@@ -0,0 +1,32 @@
+package seedu.address.logic;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Stores the history of commands executed.
+ */
+public class CommandHistory {
+ private LinkedList userInputHistory;
+
+ public CommandHistory() {
+ userInputHistory = new LinkedList<>();
+ }
+
+ /**
+ * Appends {@code userInput} to the list of user input entered.
+ */
+ public void add(String userInput) {
+ requireNonNull(userInput);
+ userInputHistory.add(userInput);
+ }
+
+ /**
+ * Returns a defensive copy of {@code userInputHistory}.
+ */
+ public List getHistory() {
+ return new LinkedList<>(userInputHistory);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/ListElementPointer.java b/src/main/java/seedu/address/logic/ListElementPointer.java
new file mode 100644
index 000000000000..21302ad1933a
--- /dev/null
+++ b/src/main/java/seedu/address/logic/ListElementPointer.java
@@ -0,0 +1,110 @@
+package seedu.address.logic;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Has a cursor that points to an element in the list, and is able to iterate through the list.
+ * This is different from {@code ListIterator}, which has a cursor that points in between elements.
+ * The {@code ListIterator}'s behaviour: when making alternating calls of {@code next()} and
+ * {@code previous()}, the same element is returned on both calls.
+ * In contrast, {@code ListElementPointer}'s behaviour: when making alternating calls of
+ * {@code next()} and {@code previous()}, the next and previous elements are returned respectively.
+ */
+public class ListElementPointer {
+ private List list;
+ private int index;
+
+ /**
+ * Constructs {@code ListElementPointer} which is backed by a defensive copy of {@code list}.
+ * The cursor points to the last element in {@code list}.
+ */
+ public ListElementPointer(List list) {
+ this.list = list;
+ index = this.list.size() - 1;
+ }
+
+ /**
+ * Appends {@code element} to the end of the list.
+ */
+ public void add(String element) {
+ list.add(element);
+ }
+
+ /**
+ * Returns true if calling {@code #next()} does not throw an {@code NoSuchElementException}.
+ */
+ public boolean hasNext() {
+ int nextIndex = index + 1;
+ return isWithinBounds(nextIndex);
+ }
+
+ /**
+ * Returns true if calling {@code #previous()} does not throw an {@code NoSuchElementException}.
+ */
+ public boolean hasPrevious() {
+ int previousIndex = index - 1;
+ return isWithinBounds(previousIndex);
+ }
+
+ /**
+ * Returns true if calling {@code #current()} does not throw an {@code NoSuchElementException}.
+ */
+ public boolean hasCurrent() {
+ return isWithinBounds(index);
+ }
+
+ private boolean isWithinBounds(int index) {
+ return index >= 0 && index < list.size();
+ }
+
+ /**
+ * Returns the next element in the list and advances the cursor position.
+ * @throws NoSuchElementException if there is no more next element in the list.
+ */
+ public String next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return list.get(++index);
+ }
+
+ /**
+ * Returns the previous element in the list and moves the cursor position backwards.
+ * @throws NoSuchElementException if there is no more previous element in the list.
+ */
+ public String previous() {
+ if (!hasPrevious()) {
+ throw new NoSuchElementException();
+ }
+ return list.get(--index);
+ }
+
+ /**
+ * Returns the current element in the list.
+ * @throws NoSuchElementException if the list is empty.
+ */
+ public String current() {
+ if (!hasCurrent()) {
+ throw new NoSuchElementException();
+ }
+ return list.get(index);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ListElementPointer)) {
+ return false;
+ }
+
+ // state check
+ ListElementPointer iterator = (ListElementPointer) other;
+ return list.equals(iterator.list) && index == iterator.index;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java
index 4df1bc65cabb..f6000c79262c 100644
--- a/src/main/java/seedu/address/logic/Logic.java
+++ b/src/main/java/seedu/address/logic/Logic.java
@@ -2,6 +2,8 @@
import javafx.collections.ObservableList;
import seedu.address.logic.commands.CommandResult;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.ReadOnlyPerson;
/**
@@ -12,10 +14,14 @@ public interface Logic {
* Executes the command and returns the result.
* @param commandText The command as entered by the user.
* @return the result of the command execution.
+ * @throws CommandException If an error occurs during command execution.
+ * @throws ParseException If an error occurs during parsing.
*/
- CommandResult execute(String commandText);
+ CommandResult execute(String commandText) throws CommandException, ParseException;
- /** Returns the filtered list of persons */
+ /** Returns an unmodifiable view of the filtered list of persons */
ObservableList getFilteredPersonList();
+ /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */
+ ListElementPointer getHistorySnapshot();
}
diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java
index ce4dc1903cff..f60c7aeebda8 100644
--- a/src/main/java/seedu/address/logic/LogicManager.java
+++ b/src/main/java/seedu/address/logic/LogicManager.java
@@ -1,16 +1,17 @@
package seedu.address.logic;
+import java.util.logging.Logger;
+
import javafx.collections.ObservableList;
import seedu.address.commons.core.ComponentManager;
import seedu.address.commons.core.LogsCenter;
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandResult;
-import seedu.address.logic.parser.Parser;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.logic.parser.AddressBookParser;
+import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.Model;
import seedu.address.model.person.ReadOnlyPerson;
-import seedu.address.storage.Storage;
-
-import java.util.logging.Logger;
/**
* The main LogicManager of the app.
@@ -19,23 +20,38 @@ public class LogicManager extends ComponentManager implements Logic {
private final Logger logger = LogsCenter.getLogger(LogicManager.class);
private final Model model;
- private final Parser parser;
+ private final CommandHistory history;
+ private final AddressBookParser addressBookParser;
+ private final UndoRedoStack undoRedoStack;
- public LogicManager(Model model, Storage storage) {
+ public LogicManager(Model model) {
this.model = model;
- this.parser = new Parser();
+ this.history = new CommandHistory();
+ this.addressBookParser = new AddressBookParser();
+ this.undoRedoStack = new UndoRedoStack();
}
@Override
- public CommandResult execute(String commandText) {
+ public CommandResult execute(String commandText) throws CommandException, ParseException {
logger.info("----------------[USER COMMAND][" + commandText + "]");
- Command command = parser.parseCommand(commandText);
- command.setData(model);
- return command.execute();
+ try {
+ Command command = addressBookParser.parseCommand(commandText);
+ command.setData(model, history, undoRedoStack);
+ CommandResult result = command.execute();
+ undoRedoStack.push(command);
+ return result;
+ } finally {
+ history.add(commandText);
+ }
}
@Override
public ObservableList getFilteredPersonList() {
return model.getFilteredPersonList();
}
+
+ @Override
+ public ListElementPointer getHistorySnapshot() {
+ return new ListElementPointer(history.getHistory());
+ }
}
diff --git a/src/main/java/seedu/address/logic/UndoRedoStack.java b/src/main/java/seedu/address/logic/UndoRedoStack.java
new file mode 100644
index 000000000000..ddb62ef0ea87
--- /dev/null
+++ b/src/main/java/seedu/address/logic/UndoRedoStack.java
@@ -0,0 +1,89 @@
+package seedu.address.logic;
+
+import java.util.Stack;
+
+import seedu.address.logic.commands.Command;
+import seedu.address.logic.commands.RedoCommand;
+import seedu.address.logic.commands.UndoCommand;
+import seedu.address.logic.commands.UndoableCommand;
+
+/**
+ * Maintains the undo-stack (the stack of commands that can be undone) and the redo-stack (the stack of
+ * commands that can be undone).
+ */
+public class UndoRedoStack {
+ private Stack undoStack;
+ private Stack redoStack;
+
+ public UndoRedoStack() {
+ undoStack = new Stack<>();
+ redoStack = new Stack<>();
+ }
+
+ /**
+ * Pushes {@code command} onto the undo-stack if it is of type {@code UndoableCommand}. Clears the redo-stack
+ * if {@code command} is not of type {@code UndoCommand} or {@code RedoCommand}.
+ */
+ public void push(Command command) {
+ if (!(command instanceof UndoCommand) && !(command instanceof RedoCommand)) {
+ redoStack.clear();
+ }
+
+ if (!(command instanceof UndoableCommand)) {
+ return;
+ }
+
+ undoStack.add((UndoableCommand) command);
+ }
+
+ /**
+ * Pops and returns the next {@code UndoableCommand} to be undone in the stack.
+ */
+ public UndoableCommand popUndo() {
+ UndoableCommand toUndo = undoStack.pop();
+ redoStack.push(toUndo);
+ return toUndo;
+ }
+
+ /**
+ * Pops and returns the next {@code UndoableCommand} to be redone in the stack.
+ */
+ public UndoableCommand popRedo() {
+ UndoableCommand toRedo = redoStack.pop();
+ undoStack.push(toRedo);
+ return toRedo;
+ }
+
+ /**
+ * Returns true if there are more commands that can be undone.
+ */
+ public boolean canUndo() {
+ return !undoStack.empty();
+ }
+
+ /**
+ * Returns true if there are more commands that can be redone.
+ */
+ public boolean canRedo() {
+ return !redoStack.empty();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof UndoRedoStack)) {
+ return false;
+ }
+
+ UndoRedoStack stack = (UndoRedoStack) other;
+
+ // state check
+ return undoStack.equals(stack.undoStack)
+ && redoStack.equals(stack.redoStack);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java
index 2860a9ab2a85..63ee5489d443 100644
--- a/src/main/java/seedu/address/logic/commands/AddCommand.java
+++ b/src/main/java/seedu/address/logic/commands/AddCommand.java
@@ -1,24 +1,47 @@
package seedu.address.logic.commands;
-import seedu.address.commons.exceptions.IllegalValueException;
-import seedu.address.model.person.*;
-import seedu.address.model.tag.Tag;
-import seedu.address.model.tag.UniqueTagList;
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
-import java.util.HashSet;
-import java.util.Set;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.DateOfBirth;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.Phone;
+import seedu.address.model.person.ReadOnlyPerson;
+import seedu.address.model.person.exceptions.DuplicatePersonException;
/**
* Adds a person to the address book.
*/
-public class AddCommand extends Command {
+public class AddCommand extends UndoableCommand {
+ public static final String[] COMMAND_WORDS = {"add", "a", "+"};
public static final String COMMAND_WORD = "add";
- public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. "
- + "Parameters: NAME p/PHONE e/EMAIL a/ADDRESS [t/TAG]...\n"
- + "Example: " + COMMAND_WORD
- + " John Doe p/98765432 e/johnd@gmail.com a/311, Clementi Ave 2, #02-25 t/friends t/owesMoney";
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ + ": Adds a person to the address book. "
+ + "Parameters: "
+ + PREFIX_NAME + "NAME "
+ + "[" + PREFIX_PHONE + "PHONE] "
+ + "[" + PREFIX_EMAIL + "EMAIL] "
+ + "[" + PREFIX_ADDRESS + "ADDRESS] "
+ + "[" + PREFIX_DOB + "DATE OF BIRTH] "
+ + "[" + PREFIX_TAG + "TAG]...\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_NAME + "John Doe "
+ + PREFIX_PHONE + "98765432 "
+ + PREFIX_EMAIL + "johnd@example.com "
+ + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 "
+ + PREFIX_DOB + "20 01 1997 "
+ + PREFIX_TAG + "friends "
+ + PREFIX_TAG + "owesMoney";
public static final String MESSAGE_SUCCESS = "New person added: %1$s";
public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book";
@@ -26,35 +49,100 @@ public class AddCommand extends Command {
private final Person toAdd;
/**
- * Convenience constructor using raw values.
- *
- * @throws IllegalValueException if any of the raw values are invalid
+ * Creates an AddCommand to add the specified {@code ReadOnlyPerson}
*/
- public AddCommand(String name, String phone, String email, String address, Set tags)
- throws IllegalValueException {
- final Set tagSet = new HashSet<>();
- for (String tagName : tags) {
- tagSet.add(new Tag(tagName));
- }
- this.toAdd = new Person(
- new Name(name),
- new Phone(phone),
- new Email(email),
- new Address(address),
- new UniqueTagList(tagSet)
- );
+ public AddCommand(ReadOnlyPerson person) {
+ toAdd = new Person(person);
}
@Override
- public CommandResult execute() {
- assert model != null;
+ public CommandResult executeUndoableCommand() throws CommandException {
+ requireNonNull(model);
try {
model.addPerson(toAdd);
return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd));
- } catch (UniquePersonList.DuplicatePersonException e) {
- return new CommandResult(MESSAGE_DUPLICATE_PERSON);
+ } catch (DuplicatePersonException e) {
+ throw new CommandException(MESSAGE_DUPLICATE_PERSON);
}
}
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof AddCommand // instanceof handles nulls
+ && toAdd.equals(((AddCommand) other).toAdd));
+ }
+
+ /**
+ * Stores the optional details to add the person with. By default each field is an object
+ * with value of empty String.
+ */
+ public static class AddPersonOptionalFieldDescriptor {
+ private Phone phone;
+ private Email email;
+ private Address address;
+ private DateOfBirth dateofbirth;
+
+ public AddPersonOptionalFieldDescriptor() {
+ this.phone = new Phone();
+ this.email = new Email();
+ this.address = new Address();
+ this.dateofbirth = new DateOfBirth();
+ }
+
+ public void setPhone(Phone phone) {
+ this.phone = phone;
+ }
+
+ public Phone getPhone() {
+ return phone;
+ }
+
+ public void setEmail(Email email) {
+ this.email = email;
+ }
+
+ public Email getEmail() {
+ return email;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setDateOfBirth(DateOfBirth dateofbirth) {
+ this.dateofbirth = dateofbirth;
+ }
+
+ public DateOfBirth getDateOfBirth() {
+ return dateofbirth;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddCommand.AddPersonOptionalFieldDescriptor)) {
+ return false;
+ }
+
+ // state check
+ AddCommand.AddPersonOptionalFieldDescriptor a =
+ (AddCommand.AddPersonOptionalFieldDescriptor) other;
+
+ return getPhone().equals(a.getPhone())
+ && getEmail().equals(a.getEmail())
+ && getAddress().equals(a.getAddress())
+ && getDateOfBirth().equals(a.getDateOfBirth());
+ }
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java
index 522d57189f51..d78b9b10b4ab 100644
--- a/src/main/java/seedu/address/logic/commands/ClearCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java
@@ -1,22 +1,23 @@
package seedu.address.logic.commands;
+import static java.util.Objects.requireNonNull;
+
import seedu.address.model.AddressBook;
/**
* Clears the address book.
*/
-public class ClearCommand extends Command {
+public class ClearCommand extends UndoableCommand {
+ public static final String[] COMMAND_WORDS = {"clear", "clr", "c", "cl"};
public static final String COMMAND_WORD = "clear";
public static final String MESSAGE_SUCCESS = "Address book has been cleared!";
- public ClearCommand() {}
-
@Override
- public CommandResult execute() {
- assert model != null;
- model.resetData(AddressBook.getEmptyAddressBook());
+ public CommandResult executeUndoableCommand() {
+ requireNonNull(model);
+ model.resetData(new AddressBook());
return new CommandResult(MESSAGE_SUCCESS);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java
index 7c0ba2fd0161..209d0032be2d 100644
--- a/src/main/java/seedu/address/logic/commands/Command.java
+++ b/src/main/java/seedu/address/logic/commands/Command.java
@@ -1,8 +1,9 @@
package seedu.address.logic.commands;
-import seedu.address.commons.core.EventsCenter;
import seedu.address.commons.core.Messages;
-import seedu.address.commons.events.ui.IncorrectCommandAttemptedEvent;
+import seedu.address.logic.CommandHistory;
+import seedu.address.logic.UndoRedoStack;
+import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
/**
@@ -10,6 +11,8 @@
*/
public abstract class Command {
protected Model model;
+ protected CommandHistory history;
+ protected UndoRedoStack undoRedoStack;
/**
* Constructs a feedback message to summarise an operation that displayed a listing of persons.
@@ -25,22 +28,30 @@ public static String getMessageForPersonListShownSummary(int displaySize) {
* Executes the command and returns the result message.
*
* @return feedback message of the operation result for display
+ * @throws CommandException If an error occurs during command execution.
*/
- public abstract CommandResult execute();
+ public abstract CommandResult execute() throws CommandException;
/**
* Provides any needed dependencies to the command.
* Commands making use of any of these should override this method to gain
* access to the dependencies.
*/
- public void setData(Model model) {
+ public void setData(Model model, CommandHistory history, UndoRedoStack undoRedoStack) {
this.model = model;
}
/**
- * Raises an event to indicate an attempt to execute an incorrect command
+ * @return concatenated form of the array of command words to allow easier printing for the help command
*/
- protected void indicateAttemptToExecuteIncorrectCommand() {
- EventsCenter.getInstance().post(new IncorrectCommandAttemptedEvent(this));
+ public static String concatenateCommandWords(String[] commandWords) {
+ String finalWord = new String();
+ for (int i = 0; i < commandWords.length - 1; i++) {
+ finalWord += commandWords[i] + " -OR- ";
+ }
+ finalWord += commandWords[commandWords.length - 1];
+ return finalWord;
}
+
+
}
diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java
index f46f2f31353e..abdc267a2c44 100644
--- a/src/main/java/seedu/address/logic/commands/CommandResult.java
+++ b/src/main/java/seedu/address/logic/commands/CommandResult.java
@@ -1,5 +1,7 @@
package seedu.address.logic.commands;
+import static java.util.Objects.requireNonNull;
+
/**
* Represents the result of a command execution.
*/
@@ -8,8 +10,7 @@ public class CommandResult {
public final String feedbackToUser;
public CommandResult(String feedbackToUser) {
- assert feedbackToUser != null;
- this.feedbackToUser = feedbackToUser;
+ this.feedbackToUser = requireNonNull(feedbackToUser);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
index 1bfebe8912a8..f011246f7ad8 100644
--- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java
+++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
@@ -1,42 +1,45 @@
package seedu.address.logic.commands;
+import java.util.List;
+
import seedu.address.commons.core.Messages;
-import seedu.address.commons.core.UnmodifiableObservableList;
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.person.ReadOnlyPerson;
-import seedu.address.model.person.UniquePersonList.PersonNotFoundException;
+import seedu.address.model.person.exceptions.PersonNotFoundException;
/**
* Deletes a person identified using it's last displayed index from the address book.
*/
-public class DeleteCommand extends Command {
+public class DeleteCommand extends UndoableCommand {
+ public static final String[] COMMAND_WORDS = {"delete", "del", "d", "-"};
public static final String COMMAND_WORD = "delete";
- public static final String MESSAGE_USAGE = COMMAND_WORD
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ ": Deletes the person identified by the index number used in the last person listing.\n"
+ "Parameters: INDEX (must be a positive integer)\n"
+ "Example: " + COMMAND_WORD + " 1";
public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s";
- public final int targetIndex;
+ private final Index targetIndex;
- public DeleteCommand(int targetIndex) {
+ public DeleteCommand(Index targetIndex) {
this.targetIndex = targetIndex;
}
@Override
- public CommandResult execute() {
+ public CommandResult executeUndoableCommand() throws CommandException {
- UnmodifiableObservableList lastShownList = model.getFilteredPersonList();
+ List lastShownList = model.getFilteredPersonList();
- if (lastShownList.size() < targetIndex) {
- indicateAttemptToExecuteIncorrectCommand();
- return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
- ReadOnlyPerson personToDelete = lastShownList.get(targetIndex - 1);
+ ReadOnlyPerson personToDelete = lastShownList.get(targetIndex.getZeroBased());
try {
model.deletePerson(personToDelete);
@@ -47,4 +50,10 @@ public CommandResult execute() {
return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete));
}
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof DeleteCommand // instanceof handles nulls
+ && this.targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java
new file mode 100644
index 000000000000..65c3b345da2f
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/EditCommand.java
@@ -0,0 +1,258 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DELTAG;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.Messages;
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.CollectionUtil;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.DateOfBirth;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.Phone;
+import seedu.address.model.person.ReadOnlyPerson;
+import seedu.address.model.person.exceptions.DuplicatePersonException;
+import seedu.address.model.person.exceptions.PersonNotFoundException;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Edits the details of an existing person in the address book.
+ */
+public class EditCommand extends UndoableCommand {
+
+ public static final String[] COMMAND_WORDS = {"edit", "e", "change", "ed"};
+ public static final String COMMAND_WORD = "edit";
+
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ + ": Edits the details of the person identified "
+ + "by the index number used in the last person listing. "
+ + "Existing values will be overwritten by the input values.\n"
+ + "Parameters: INDEX (must be a positive integer) "
+ + "[" + PREFIX_NAME + "NAME] "
+ + "[" + PREFIX_PHONE + "PHONE] "
+ + "[" + PREFIX_EMAIL + "EMAIL] "
+ + "[" + PREFIX_ADDRESS + "ADDRESS] "
+ + "[" + PREFIX_DOB + "DATE OF BIRTH] "
+ + "[" + PREFIX_TAG + "TAG] "
+ + "[" + PREFIX_DELTAG + "TAG_TO_DELETE] ...\n"
+ + "Example: " + COMMAND_WORD + " 1 "
+ + PREFIX_PHONE + "91234567 "
+ + PREFIX_EMAIL + "johndoe@example.com";
+
+ public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s";
+ public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.";
+ public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book.";
+
+ private final Index index;
+ private final EditPersonDescriptor editPersonDescriptor;
+
+ /**
+ * @param index of the person in the filtered person list to edit
+ * @param editPersonDescriptor details to edit the person with
+ */
+ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) {
+ requireNonNull(index);
+ requireNonNull(editPersonDescriptor);
+
+ this.index = index;
+ this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor);
+ }
+
+ @Override
+ public CommandResult executeUndoableCommand() throws CommandException {
+ List lastShownList = model.getFilteredPersonList();
+
+ if (index.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ ReadOnlyPerson personToEdit = lastShownList.get(index.getZeroBased());
+ Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor);
+
+ try {
+ model.updatePerson(personToEdit, editedPerson);
+ } catch (DuplicatePersonException dpe) {
+ throw new CommandException(MESSAGE_DUPLICATE_PERSON);
+ } catch (PersonNotFoundException pnfe) {
+ throw new AssertionError("The target person cannot be missing");
+ }
+ model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson));
+ }
+
+ /**
+ * Creates and returns a {@code Person} with the details of {@code personToEdit}
+ * edited with {@code editPersonDescriptor}.
+ */
+ private static Person createEditedPerson(ReadOnlyPerson personToEdit,
+ EditPersonDescriptor editPersonDescriptor) {
+ assert personToEdit != null;
+
+ Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName());
+ Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone());
+ Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail());
+ Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress());
+ DateOfBirth updatedDateOfBirth = editPersonDescriptor.getDateOfBirth().orElse(personToEdit.getDateOfBirth());
+ Set updatedTags = personToEdit.getTags();
+
+ if (editPersonDescriptor.getTagsToDel().isPresent()) {
+ for (Tag tag : editPersonDescriptor.getTagsToDel().get()) {
+ if (tag.getTagName().equals("all")) {
+ updatedTags.clear();
+ }
+ }
+ updatedTags.removeAll(editPersonDescriptor.getTagsToDel().get());
+ }
+
+ if (editPersonDescriptor.getTags().isPresent()) {
+ updatedTags.addAll(editPersonDescriptor.getTags().get());
+ }
+
+ return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedDateOfBirth, updatedTags);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditCommand)) {
+ return false;
+ }
+
+ // state check
+ EditCommand e = (EditCommand) other;
+ return index.equals(e.index)
+ && editPersonDescriptor.equals(e.editPersonDescriptor);
+ }
+
+ /**
+ * Stores the details to edit the person with. Each non-empty field value will replace the
+ * corresponding field value of the person.
+ */
+ public static class EditPersonDescriptor {
+ private Name name;
+ private Phone phone;
+ private Email email;
+ private Address address;
+ private DateOfBirth dob;
+ private Set tags;
+ private Set tagsToDel;
+
+ public EditPersonDescriptor() {}
+
+ public EditPersonDescriptor(EditPersonDescriptor toCopy) {
+ this.name = toCopy.name;
+ this.phone = toCopy.phone;
+ this.email = toCopy.email;
+ this.address = toCopy.address;
+ this.dob = toCopy.dob;
+ this.tags = toCopy.tags;
+ this.tagsToDel = toCopy.tagsToDel;
+ }
+
+ /**
+ * Returns true if at least one field is edited.
+ */
+ public boolean isAnyFieldEdited() {
+ return CollectionUtil.isAnyNonNull(
+ this.name, this.phone, this.email, this.address, this.dob, this.tags, this.tagsToDel);
+ }
+
+ public void setName(Name name) {
+ this.name = name;
+ }
+
+ public Optional getName() {
+ return Optional.ofNullable(name);
+ }
+
+ public void setPhone(Phone phone) {
+ this.phone = phone;
+ }
+
+ public Optional getPhone() {
+ return Optional.ofNullable(phone);
+ }
+
+ public void setEmail(Email email) {
+ this.email = email;
+ }
+
+ public Optional getEmail() {
+ return Optional.ofNullable(email);
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public Optional getAddress() {
+ return Optional.ofNullable(address);
+ }
+
+ public void setDateOfBirth(DateOfBirth dob) {
+ this.dob = dob;
+ }
+
+ public Optional getDateOfBirth() {
+ return Optional.ofNullable(dob);
+ }
+
+ public void setTags(Set tags) {
+ this.tags = tags;
+ }
+
+ public Optional> getTags() {
+ return Optional.ofNullable(tags);
+ }
+
+ public void setTagsToDel(Set tagsToDel) {
+ this.tagsToDel = tagsToDel;
+ }
+
+ public Optional> getTagsToDel() {
+ return Optional.ofNullable(tagsToDel);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // short circuit if same object
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditPersonDescriptor)) {
+ return false;
+ }
+
+ // state check
+ EditPersonDescriptor e = (EditPersonDescriptor) other;
+
+ return getName().equals(e.getName())
+ && getPhone().equals(e.getPhone())
+ && getEmail().equals(e.getEmail())
+ && getAddress().equals(e.getAddress())
+ && getDateOfBirth().equals(e.getDateOfBirth())
+ && getTags().equals(e.getTags());
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java
index d98233ce2a0b..bae425aef902 100644
--- a/src/main/java/seedu/address/logic/commands/ExitCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java
@@ -8,12 +8,11 @@
*/
public class ExitCommand extends Command {
+ public static final String[] COMMAND_WORDS = {"exit", "quit", "esc", "off"};
public static final String COMMAND_WORD = "exit";
public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ...";
- public ExitCommand() {}
-
@Override
public CommandResult execute() {
EventsCenter.getInstance().post(new ExitAppRequestEvent());
diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java
index 1d61bf6cc857..cef2381b2190 100644
--- a/src/main/java/seedu/address/logic/commands/FindCommand.java
+++ b/src/main/java/seedu/address/logic/commands/FindCommand.java
@@ -1,6 +1,6 @@
package seedu.address.logic.commands;
-import java.util.Set;
+import seedu.address.model.person.NameContainsKeywordsPredicate;
/**
* Finds and lists all persons in address book whose name contains any of the argument keywords.
@@ -8,23 +8,31 @@
*/
public class FindCommand extends Command {
+ public static final String[] COMMAND_WORDS = {"find", "f", "look", "lookup"};
public static final String COMMAND_WORD = "find";
- public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of "
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ + ": Finds all persons whose names contain any of "
+ "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n"
+ "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ "Example: " + COMMAND_WORD + " alice bob charlie";
- private final Set keywords;
+ private final NameContainsKeywordsPredicate predicate;
- public FindCommand(Set keywords) {
- this.keywords = keywords;
+ public FindCommand(NameContainsKeywordsPredicate predicate) {
+ this.predicate = predicate;
}
@Override
public CommandResult execute() {
- model.updateFilteredPersonList(keywords);
+ model.updateFilteredPersonList(predicate);
return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size()));
}
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof FindCommand // instanceof handles nulls
+ && this.predicate.equals(((FindCommand) other).predicate)); // state check
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java
index 65af96940242..43a86000e672 100644
--- a/src/main/java/seedu/address/logic/commands/HelpCommand.java
+++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java
@@ -1,6 +1,5 @@
package seedu.address.logic.commands;
-
import seedu.address.commons.core.EventsCenter;
import seedu.address.commons.events.ui.ShowHelpRequestEvent;
@@ -9,15 +8,15 @@
*/
public class HelpCommand extends Command {
+ public static final String[] COMMAND_WORDS = {"help", "h", "hlp", "f1", "commands", "command", "sos"};
public static final String COMMAND_WORD = "help";
- public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n"
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ + ": Shows program usage instructions.\n"
+ "Example: " + COMMAND_WORD;
public static final String SHOWING_HELP_MESSAGE = "Opened help window.";
- public HelpCommand() {}
-
@Override
public CommandResult execute() {
EventsCenter.getInstance().post(new ShowHelpRequestEvent());
diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java
new file mode 100644
index 000000000000..4345ab13eeb6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/HistoryCommand.java
@@ -0,0 +1,39 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.List;
+
+import seedu.address.logic.CommandHistory;
+import seedu.address.logic.UndoRedoStack;
+import seedu.address.model.Model;
+
+/**
+ * Lists all the commands entered by user from the start of app launch.
+ */
+public class HistoryCommand extends Command {
+
+ public static final String[] COMMAND_WORDS = {"history", "last", "h", "hist"};
+ public static final String COMMAND_WORD = "history";
+ public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s";
+ public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands.";
+
+ @Override
+ public CommandResult execute() {
+ List previousCommands = history.getHistory();
+
+ if (previousCommands.isEmpty()) {
+ return new CommandResult(MESSAGE_NO_HISTORY);
+ }
+
+ Collections.reverse(previousCommands);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, String.join("\n", previousCommands)));
+ }
+
+ @Override
+ public void setData(Model model, CommandHistory history, UndoRedoStack undoRedoStack) {
+ requireNonNull(history);
+ this.history = history;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java b/src/main/java/seedu/address/logic/commands/IncorrectCommand.java
deleted file mode 100644
index 491d9cb9da35..000000000000
--- a/src/main/java/seedu/address/logic/commands/IncorrectCommand.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package seedu.address.logic.commands;
-
-
-/**
- * Represents an incorrect command. Upon execution, produces some feedback to the user.
- */
-public class IncorrectCommand extends Command {
-
- public final String feedbackToUser;
-
- public IncorrectCommand(String feedbackToUser){
- this.feedbackToUser = feedbackToUser;
- }
-
- @Override
- public CommandResult execute() {
- indicateAttemptToExecuteIncorrectCommand();
- return new CommandResult(feedbackToUser);
- }
-
-}
-
diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java
index 9bdd457a1b01..0696e7ee7fe8 100644
--- a/src/main/java/seedu/address/logic/commands/ListCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ListCommand.java
@@ -1,20 +1,21 @@
package seedu.address.logic.commands;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
/**
* Lists all persons in the address book to the user.
*/
public class ListCommand extends Command {
+ public static final String[] COMMAND_WORDS = {"list"};
public static final String COMMAND_WORD = "list";
public static final String MESSAGE_SUCCESS = "Listed all persons";
- public ListCommand() {}
@Override
public CommandResult execute() {
- model.updateFilteredListToShowAll();
+ model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
return new CommandResult(MESSAGE_SUCCESS);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/PartialFindCommand.java b/src/main/java/seedu/address/logic/commands/PartialFindCommand.java
new file mode 100644
index 000000000000..0cdc6f0bae39
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/PartialFindCommand.java
@@ -0,0 +1,38 @@
+package seedu.address.logic.commands;
+
+import seedu.address.model.person.NameStartsWithKeywordsPredicate;
+
+/**
+ * Finds and lists all persons in address book whose name starts with any of the argument keywords.
+ * Keyword matching is case sensitive.
+ */
+public class PartialFindCommand extends Command {
+
+ public static final String[] COMMAND_WORDS = {"pfind", "pf", "plook", "plookup"};
+ public static final String COMMAND_WORD = "pfind";
+
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ + ": Finds all persons whose names starts with "
+ + "the specified keywords (case-sensitive) and displays them as a list with index numbers.\n"
+ + "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ + "Example: " + COMMAND_WORD + " Ali Bo Ch";
+
+ private final NameStartsWithKeywordsPredicate predicate;
+
+ public PartialFindCommand (NameStartsWithKeywordsPredicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public CommandResult execute() {
+ model.updateFilteredPersonList(predicate);
+ return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size()));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof PartialFindCommand // instanceof handles nulls
+ && this.predicate.equals(((PartialFindCommand) other).predicate)); // state check
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java
new file mode 100644
index 000000000000..2469ea86c922
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java
@@ -0,0 +1,37 @@
+package seedu.address.logic.commands;
+
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import seedu.address.logic.CommandHistory;
+import seedu.address.logic.UndoRedoStack;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+
+/**
+ * Redo the previously undone command.
+ */
+public class RedoCommand extends Command {
+
+ public static final String[] COMMAND_WORDS = {"redo", "r"};
+ public static final String COMMAND_WORD = "redo";
+ public static final String MESSAGE_SUCCESS = "Redo success!";
+ public static final String MESSAGE_FAILURE = "No more commands to redo!";
+
+ @Override
+ public CommandResult execute() throws CommandException {
+ requireAllNonNull(model, undoRedoStack);
+
+ if (!undoRedoStack.canRedo()) {
+ throw new CommandException(MESSAGE_FAILURE);
+ }
+
+ undoRedoStack.popRedo().redo();
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+
+ @Override
+ public void setData(Model model, CommandHistory commandHistory, UndoRedoStack undoRedoStack) {
+ this.model = model;
+ this.undoRedoStack = undoRedoStack;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java
index 9ca0551f1951..659dc22a4a3e 100644
--- a/src/main/java/seedu/address/logic/commands/SelectCommand.java
+++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java
@@ -1,9 +1,12 @@
package seedu.address.logic.commands;
+import java.util.List;
+
import seedu.address.commons.core.EventsCenter;
import seedu.address.commons.core.Messages;
+import seedu.address.commons.core.index.Index;
import seedu.address.commons.events.ui.JumpToListRequestEvent;
-import seedu.address.commons.core.UnmodifiableObservableList;
+import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.person.ReadOnlyPerson;
/**
@@ -11,34 +14,40 @@
*/
public class SelectCommand extends Command {
- public final int targetIndex;
-
+ public static final String[] COMMAND_WORDS = {"select", "s", "choose", "sel"};
public static final String COMMAND_WORD = "select";
- public static final String MESSAGE_USAGE = COMMAND_WORD
+ public static final String MESSAGE_USAGE = concatenateCommandWords(COMMAND_WORDS)
+ ": Selects the person identified by the index number used in the last person listing.\n"
+ "Parameters: INDEX (must be a positive integer)\n"
+ "Example: " + COMMAND_WORD + " 1";
public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s";
- public SelectCommand(int targetIndex) {
+ private final Index targetIndex;
+
+ public SelectCommand(Index targetIndex) {
this.targetIndex = targetIndex;
}
@Override
- public CommandResult execute() {
+ public CommandResult execute() throws CommandException {
- UnmodifiableObservableList lastShownList = model.getFilteredPersonList();
+ List lastShownList = model.getFilteredPersonList();
- if (lastShownList.size() < targetIndex) {
- indicateAttemptToExecuteIncorrectCommand();
- return new CommandResult(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
}
- EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex - 1));
- return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex));
+ EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex));
+ return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased()));
}
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof SelectCommand // instanceof handles nulls
+ && this.targetIndex.equals(((SelectCommand) other).targetIndex)); // state check
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java
new file mode 100644
index 000000000000..90d0981c869c
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java
@@ -0,0 +1,37 @@
+package seedu.address.logic.commands;
+
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import seedu.address.logic.CommandHistory;
+import seedu.address.logic.UndoRedoStack;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+
+/**
+ * Undo the previous {@code UndoableCommand}.
+ */
+public class UndoCommand extends Command {
+
+ public static final String[] COMMAND_WORDS = {"undo", "u", "revert"};
+ public static final String COMMAND_WORD = "undo";
+ public static final String MESSAGE_SUCCESS = "Undo success!";
+ public static final String MESSAGE_FAILURE = "No more commands to undo!";
+
+ @Override
+ public CommandResult execute() throws CommandException {
+ requireAllNonNull(model, undoRedoStack);
+
+ if (!undoRedoStack.canUndo()) {
+ throw new CommandException(MESSAGE_FAILURE);
+ }
+
+ undoRedoStack.popUndo().undo();
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+
+ @Override
+ public void setData(Model model, CommandHistory commandHistory, UndoRedoStack undoRedoStack) {
+ this.model = model;
+ this.undoRedoStack = undoRedoStack;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/UndoableCommand.java b/src/main/java/seedu/address/logic/commands/UndoableCommand.java
new file mode 100644
index 000000000000..1ba888ead594
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/UndoableCommand.java
@@ -0,0 +1,58 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
+
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.AddressBook;
+import seedu.address.model.ReadOnlyAddressBook;
+
+/**
+ * Represents a command which can be undone and redone.
+ */
+public abstract class UndoableCommand extends Command {
+ private ReadOnlyAddressBook previousAddressBook;
+
+ protected abstract CommandResult executeUndoableCommand() throws CommandException;
+
+ /**
+ * Stores the current state of {@code model#addressBook}.
+ */
+ private void saveAddressBookSnapshot() {
+ requireNonNull(model);
+ this.previousAddressBook = new AddressBook(model.getAddressBook());
+ }
+
+ /**
+ * Reverts the AddressBook to the state before this command
+ * was executed and updates the filtered person list to
+ * show all persons.
+ */
+ protected final void undo() {
+ requireAllNonNull(model, previousAddressBook);
+ model.resetData(previousAddressBook);
+ model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ }
+
+ /**
+ * Executes the command and updates the filtered person
+ * list to show all persons.
+ */
+ protected final void redo() {
+ requireNonNull(model);
+ try {
+ executeUndoableCommand();
+ } catch (CommandException ce) {
+ throw new AssertionError("The command has been successfully executed previously; "
+ + "it should not fail now");
+ }
+ model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
+ }
+
+ @Override
+ public final CommandResult execute() throws CommandException {
+ saveAddressBookSnapshot();
+ return executeUndoableCommand();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/WhyCommand.java b/src/main/java/seedu/address/logic/commands/WhyCommand.java
new file mode 100644
index 000000000000..e4671bc3d009
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/WhyCommand.java
@@ -0,0 +1,54 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.ReadOnlyPerson;
+
+
+/**
+ * Format full help instructions for every command for display.
+ */
+public class WhyCommand extends Command {
+
+ public static final String[] COMMAND_WORDS = {"why"};
+ public static final String COMMAND_WORD = "why";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Tells you why.\n"
+ + "Example: " + COMMAND_WORD;
+
+ public static final String MESSAGE_WHY_REMARK_SUCCESS = "Added remark to Person: %1$s";
+ public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book.";
+
+ public static final String SHOWING_WHY_MESSAGE = "Because %1$s lives in \n%2$s";
+
+ private final Index targetIndex;
+
+ public WhyCommand(Index targetIndex) {
+ requireNonNull(targetIndex);
+
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute() {
+ //EventsCenter.getInstance().post(new ShowHelpRequestEvent());
+
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ //throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ ReadOnlyPerson personToDelete = lastShownList.get(targetIndex.getZeroBased());
+ Name name = personToDelete.getName();
+ Address address = personToDelete.getAddress();
+ String reason = personToDelete.getReason();
+ //return new CommandResult(String.format(SHOWING_WHY_MESSAGE, name, address));
+ return new CommandResult(reason);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java
new file mode 100644
index 000000000000..ed23ad42eb26
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java
@@ -0,0 +1,10 @@
+package seedu.address.logic.commands.exceptions;
+
+/**
+ * Represents an error which occurs during execution of a {@link Command}.
+ */
+public class CommandException extends Exception {
+ public CommandException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java
new file mode 100644
index 000000000000..f8ab976109bb
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java
@@ -0,0 +1,82 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.Set;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.commands.AddCommand;
+import seedu.address.logic.commands.AddCommand.AddPersonOptionalFieldDescriptor;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.DateOfBirth;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.Phone;
+import seedu.address.model.person.ReadOnlyPerson;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a new AddCommand object
+ */
+public class AddCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddCommand
+ * and returns an AddCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap;
+ argMultimap = ArgumentTokenizer.tokenize(
+ args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_DOB, PREFIX_TAG);
+
+ if (!isNamePrefixPresent(argMultimap, PREFIX_NAME)) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE));
+ }
+
+ AddPersonOptionalFieldDescriptor addPersonOptionalFieldDescriptor =
+ new AddPersonOptionalFieldDescriptor();
+
+ try {
+ Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME)).get();
+ Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG));
+
+ ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE))
+ .ifPresent(addPersonOptionalFieldDescriptor::setPhone);
+ ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL))
+ .ifPresent(addPersonOptionalFieldDescriptor::setEmail);
+ ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS))
+ .ifPresent(addPersonOptionalFieldDescriptor::setAddress);
+ ParserUtil.parseDateOfBirth(argMultimap.getValue(PREFIX_DOB))
+ .ifPresent(addPersonOptionalFieldDescriptor::setDateOfBirth);
+
+ Phone phone = addPersonOptionalFieldDescriptor.getPhone();
+ Email email = addPersonOptionalFieldDescriptor.getEmail();
+ Address address = addPersonOptionalFieldDescriptor.getAddress();
+ DateOfBirth dob = addPersonOptionalFieldDescriptor.getDateOfBirth();
+
+ ReadOnlyPerson person = new Person(name, phone, email, address, dob, tagList);
+
+ return new AddCommand(person);
+ } catch (IllegalValueException ive) {
+ throw new ParseException(ive.getMessage(), ive);
+ }
+ }
+
+ /**
+ * Returns true if the name prefixes does not contain empty {@code Optional} values in the given
+ * {@code ArgumentMultimap}.
+ */
+ private static boolean isNamePrefixPresent(ArgumentMultimap argumentMultimap, Prefix namePrefix) {
+ return argumentMultimap.getValue(namePrefix).isPresent();
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java
new file mode 100644
index 000000000000..aaa5f32c08bf
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java
@@ -0,0 +1,191 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import seedu.address.logic.commands.AddCommand;
+import seedu.address.logic.commands.ClearCommand;
+import seedu.address.logic.commands.Command;
+import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.commands.EditCommand;
+import seedu.address.logic.commands.ExitCommand;
+import seedu.address.logic.commands.FindCommand;
+import seedu.address.logic.commands.HelpCommand;
+import seedu.address.logic.commands.HistoryCommand;
+import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.PartialFindCommand;
+import seedu.address.logic.commands.RedoCommand;
+import seedu.address.logic.commands.SelectCommand;
+import seedu.address.logic.commands.UndoCommand;
+import seedu.address.logic.commands.WhyCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses user input.
+ */
+public class AddressBookParser {
+
+ /**
+ * Enumerator list to define the types of commands.
+ */
+ private enum CommandType {
+ ADD, CLEAR, DEL, EDIT, EXIT, FIND, PFIND, HELP, HISTORY, LIST, REDO, UNDO, SELECT, WHY, NONE
+ }
+
+ /**
+ * Used for initial separation of command word and args.
+ */
+ private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)");
+
+ /**
+ * Parses user input into command for execution.
+ *
+ * @param userInput full user input string
+ * @return the command based on the user input
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public Command parseCommand(String userInput) throws ParseException {
+ final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim());
+ if (!matcher.matches()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE));
+ }
+
+ final String commandWord = matcher.group("commandWord");
+ final String arguments = matcher.group("arguments");
+
+ CommandType commandType = getCommandType(commandWord.toLowerCase());
+
+ switch (commandType) {
+
+ case ADD:
+ return new AddCommandParser().parse(arguments);
+
+ case EDIT:
+ return new EditCommandParser().parse(arguments);
+
+ case SELECT:
+ return new SelectCommandParser().parse(arguments);
+
+ case DEL:
+ return new DeleteCommandParser().parse(arguments);
+
+ case CLEAR:
+ return new ClearCommand();
+
+ case FIND:
+ return new FindCommandParser().parse(arguments);
+
+ case PFIND:
+ return new PartialFindCommandParser().parse(arguments);
+
+ case LIST:
+ return new ListCommand();
+
+ case HISTORY:
+ return new HistoryCommand();
+
+ case EXIT:
+ return new ExitCommand();
+
+ case HELP:
+ return new HelpCommand();
+
+ case UNDO:
+ return new UndoCommand();
+
+ case REDO:
+ return new RedoCommand();
+
+ case WHY:
+ return new WhyCommandParser().parse(arguments);
+
+ default:
+ throw new ParseException(MESSAGE_UNKNOWN_COMMAND);
+ }
+ }
+
+ /**
+ * Searches the entire list of acceptable command words in each command and returns the enumerated value type.
+ * @param commandWord
+ * @return enumerated value for the switch statement to process
+ */
+
+ private CommandType getCommandType(String commandWord) {
+ for (String word : AddCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.ADD;
+ }
+ }
+ for (String word : ClearCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.CLEAR;
+ }
+ }
+ for (String word : DeleteCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.DEL;
+ }
+ }
+ for (String word : EditCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.EDIT;
+ }
+ }
+ for (String word : ExitCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.EXIT;
+ }
+ }
+ for (String word : FindCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.FIND;
+ }
+ }
+ for (String word : PartialFindCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.PFIND;
+ }
+ }
+ for (String word : HelpCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.HELP;
+ }
+ }
+ for (String word : HistoryCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.HISTORY;
+ }
+ }
+ for (String word : ListCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.LIST;
+ }
+ }
+ for (String word : RedoCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.REDO;
+ }
+ }
+ for (String word : SelectCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.SELECT;
+ }
+ }
+ for (String word : UndoCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.UNDO;
+ }
+ }
+ for (String word : WhyCommand.COMMAND_WORDS) {
+ if (commandWord.contentEquals(word)) {
+ return CommandType.WHY;
+ }
+ }
+ return CommandType.NONE;
+ }
+
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java
new file mode 100644
index 000000000000..954c8e18f8ea
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java
@@ -0,0 +1,60 @@
+package seedu.address.logic.parser;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Stores mapping of prefixes to their respective arguments.
+ * Each key may be associated with multiple argument values.
+ * Values for a given key are stored in a list, and the insertion ordering is maintained.
+ * Keys are unique, but the list of argument values may contain duplicate argument values, i.e. the same argument value
+ * can be inserted multiple times for the same prefix.
+ */
+public class ArgumentMultimap {
+
+ /** Prefixes mapped to their respective arguments**/
+ private final Map> argMultimap = new HashMap<>();
+
+ /**
+ * Associates the specified argument value with {@code prefix} key in this map.
+ * If the map previously contained a mapping for the key, the new value is appended to the list of existing values.
+ *
+ * @param prefix Prefix key with which the specified argument value is to be associated
+ * @param argValue Argument value to be associated with the specified prefix key
+ */
+ public void put(Prefix prefix, String argValue) {
+ List argValues = getAllValues(prefix);
+ argValues.add(argValue);
+ argMultimap.put(prefix, argValues);
+ }
+
+ /**
+ * Returns the last value of {@code prefix}.
+ */
+ public Optional getValue(Prefix prefix) {
+ List values = getAllValues(prefix);
+ return values.isEmpty() ? Optional.empty() : Optional.of(values.get(values.size() - 1));
+ }
+
+ /**
+ * Returns all values of {@code prefix}.
+ * If the prefix does not exist or has no values, this will return an empty list.
+ * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap.
+ */
+ public List getAllValues(Prefix prefix) {
+ if (!argMultimap.containsKey(prefix)) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(argMultimap.get(prefix));
+ }
+
+ /**
+ * Returns the preamble (text before the first valid prefix). Trims any leading/trailing spaces.
+ */
+ public String getPreamble() {
+ return getValue(new Prefix("")).orElse("");
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
new file mode 100644
index 000000000000..a1bddbb6b979
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java
@@ -0,0 +1,150 @@
+package seedu.address.logic.parser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tokenizes arguments string of the form: {@code preamble value value ...}
+ * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
+ * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
+ * 2. Leading and trailing whitespaces of an argument value will be discarded.
+ * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/}
+ * in the above example.
+ */
+public class ArgumentTokenizer {
+
+ /**
+ * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps prefixes to their
+ * respective argument values. Only the given prefixes will be recognized in the arguments string.
+ *
+ * @param argsString Arguments string of the form: {@code preamble value value ...}
+ * @param prefixes Prefixes to tokenize the arguments string with
+ * @return ArgumentMultimap object that maps prefixes to their arguments
+ */
+ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) {
+ List positions = findAllPrefixPositions(argsString, prefixes);
+ return extractArguments(argsString, positions);
+ }
+
+ /**
+ * Finds all zero-based prefix positions in the given arguments string.
+ *
+ * @param argsString Arguments string of the form: {@code preamble value value ...}
+ * @param prefixes Prefixes to find in the arguments string
+ * @return List of zero-based prefix positions in the given arguments string
+ */
+ private static List findAllPrefixPositions(String argsString, Prefix... prefixes) {
+ List positions = new ArrayList<>();
+
+ for (Prefix prefix : prefixes) {
+ positions.addAll(findPrefixPositions(argsString, prefix));
+ }
+
+ return positions;
+ }
+
+ /**
+ * {@see findAllPrefixPositions}
+ */
+ private static List findPrefixPositions(String argsString, Prefix prefix) {
+ List positions = new ArrayList<>();
+
+ int prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), 0);
+ while (prefixPosition != -1) {
+ PrefixPosition extendedPrefix = new PrefixPosition(prefix, prefixPosition);
+ positions.add(extendedPrefix);
+ prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition);
+ }
+
+ return positions;
+ }
+
+ /**
+ * Returns the index of the first occurrence of {@code prefix} in
+ * {@code argsString} starting from index {@code fromIndex}. An occurrence
+ * is valid if there is a whitespace before {@code prefix}. Returns -1 if no
+ * such occurrence can be found.
+ *
+ * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and
+ * {@code fromIndex} = 0, this method returns -1 as there are no valid
+ * occurrences of "p/" with whitespace before it. However, if
+ * {@code argsString} = "e/hi p/900", {@code prefix} = "p/" and
+ * {@code fromIndex} = 0, this method returns 5.
+ */
+ private static int findPrefixPosition(String argsString, String prefix, int fromIndex) {
+ int prefixIndex = argsString.indexOf(" " + prefix, fromIndex);
+ return prefixIndex == -1 ? -1
+ : prefixIndex + 1; // +1 as offset for whitespace
+ }
+
+ /**
+ * Extracts prefixes and their argument values, and returns an {@code ArgumentMultimap} object that maps the
+ * extracted prefixes to their respective arguments. Prefixes are extracted based on their zero-based positions in
+ * {@code argsString}.
+ *
+ * @param argsString Arguments string of the form: {@code preamble value value ...}
+ * @param prefixPositions Zero-based positions of all prefixes in {@code argsString}
+ * @return ArgumentMultimap object that maps prefixes to their arguments
+ */
+ private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) {
+
+ // Sort by start position
+ prefixPositions.sort((prefix1, prefix2) -> prefix1.getStartPosition() - prefix2.getStartPosition());
+
+ // Insert a PrefixPosition to represent the preamble
+ PrefixPosition preambleMarker = new PrefixPosition(new Prefix(""), 0);
+ prefixPositions.add(0, preambleMarker);
+
+ // Add a dummy PrefixPosition to represent the end of the string
+ PrefixPosition endPositionMarker = new PrefixPosition(new Prefix(""), argsString.length());
+ prefixPositions.add(endPositionMarker);
+
+ // Map prefixes to their argument values (if any)
+ ArgumentMultimap argMultimap = new ArgumentMultimap();
+ for (int i = 0; i < prefixPositions.size() - 1; i++) {
+ // Extract and store prefixes and their arguments
+ Prefix argPrefix = prefixPositions.get(i).getPrefix();
+ String argValue = extractArgumentValue(argsString, prefixPositions.get(i), prefixPositions.get(i + 1));
+ argMultimap.put(argPrefix, argValue);
+ }
+
+ return argMultimap;
+ }
+
+ /**
+ * Returns the trimmed value of the argument in the arguments string specified by {@code currentPrefixPosition}.
+ * The end position of the value is determined by {@code nextPrefixPosition}.
+ */
+ private static String extractArgumentValue(String argsString,
+ PrefixPosition currentPrefixPosition,
+ PrefixPosition nextPrefixPosition) {
+ Prefix prefix = currentPrefixPosition.getPrefix();
+
+ int valueStartPos = currentPrefixPosition.getStartPosition() + prefix.getPrefix().length();
+ String value = argsString.substring(valueStartPos, nextPrefixPosition.getStartPosition());
+
+ return value.trim();
+ }
+
+ /**
+ * Represents a prefix's position in an arguments string.
+ */
+ private static class PrefixPosition {
+ private int startPosition;
+ private final Prefix prefix;
+
+ PrefixPosition(Prefix prefix, int startPosition) {
+ this.prefix = prefix;
+ this.startPosition = startPosition;
+ }
+
+ int getStartPosition() {
+ return this.startPosition;
+ }
+
+ Prefix getPrefix() {
+ return this.prefix;
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java
new file mode 100644
index 000000000000..d9a1c1cc4ff2
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java
@@ -0,0 +1,16 @@
+package seedu.address.logic.parser;
+
+/**
+ * Contains Command Line Interface (CLI) syntax definitions common to multiple commands
+ */
+public class CliSyntax {
+
+ /* Prefix definitions */
+ public static final Prefix PREFIX_NAME = new Prefix("n/");
+ public static final Prefix PREFIX_PHONE = new Prefix("p/");
+ public static final Prefix PREFIX_EMAIL = new Prefix("e/");
+ public static final Prefix PREFIX_ADDRESS = new Prefix("a/");
+ public static final Prefix PREFIX_TAG = new Prefix("t/");
+ public static final Prefix PREFIX_DELTAG = new Prefix("dt/");
+ public static final Prefix PREFIX_DOB = new Prefix("d/");
+}
diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java
new file mode 100644
index 000000000000..fe9c1653850e
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java
@@ -0,0 +1,30 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteCommand object
+ */
+public class DeleteCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteCommand
+ * and returns an DeleteCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteCommand(index);
+ } catch (IllegalValueException ive) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE));
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
new file mode 100644
index 000000000000..86f171eb427e
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
@@ -0,0 +1,86 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DELTAG;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.commands.EditCommand;
+import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parses input arguments and creates a new EditCommand object
+ */
+public class EditCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the EditCommand
+ * and returns an EditCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public EditCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(
+ args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS,
+ PREFIX_DOB, PREFIX_TAG, PREFIX_DELTAG);
+
+ Index index;
+
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (IllegalValueException ive) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE));
+ }
+
+ EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor();
+ try {
+ ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME)).ifPresent(editPersonDescriptor::setName);
+ ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE)).ifPresent(editPersonDescriptor::setPhone);
+ ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL)).ifPresent(editPersonDescriptor::setEmail);
+ ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS)).ifPresent(editPersonDescriptor::setAddress);
+ ParserUtil.parseDateOfBirth(argMultimap.getValue(PREFIX_DOB))
+ .ifPresent(editPersonDescriptor::setDateOfBirth);
+ parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags);
+ parseTagsForEdit(argMultimap.getAllValues(PREFIX_DELTAG)).ifPresent(editPersonDescriptor::setTagsToDel);
+ } catch (IllegalValueException ive) {
+ throw new ParseException(ive.getMessage(), ive);
+ }
+
+ if (!editPersonDescriptor.isAnyFieldEdited()) {
+ throw new ParseException(EditCommand.MESSAGE_NOT_EDITED);
+ }
+
+ return new EditCommand(index, editPersonDescriptor);
+ }
+
+ /**
+ * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty.
+ * If {@code tags} contain only one element which is an empty string, it will be parsed into a
+ * {@code Set} containing zero tags.
+ */
+ private Optional> parseTagsForEdit(Collection tags) throws IllegalValueException {
+ assert tags != null;
+
+ if (tags.isEmpty()) {
+ return Optional.empty();
+ }
+ Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags;
+ return Optional.of(ParserUtil.parseTags(tagSet));
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java
new file mode 100644
index 000000000000..b186a967cb94
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java
@@ -0,0 +1,33 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.util.Arrays;
+
+import seedu.address.logic.commands.FindCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.NameContainsKeywordsPredicate;
+
+/**
+ * Parses input arguments and creates a new FindCommand object
+ */
+public class FindCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the FindCommand
+ * and returns an FindCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public FindCommand parse(String args) throws ParseException {
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
+ }
+
+ String[] nameKeywords = trimmedArgs.split("\\s+");
+
+ return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords)));
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java
index 959b2cd0383c..d6551ad8e3ff 100644
--- a/src/main/java/seedu/address/logic/parser/Parser.java
+++ b/src/main/java/seedu/address/logic/parser/Parser.java
@@ -1,192 +1,16 @@
package seedu.address.logic.parser;
-import seedu.address.logic.commands.*;
-import seedu.address.commons.util.StringUtil;
-import seedu.address.commons.exceptions.IllegalValueException;
-
-import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
-import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND;
+import seedu.address.logic.commands.Command;
+import seedu.address.logic.parser.exceptions.ParseException;
/**
- * Parses user input.
+ * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}.
*/
-public class Parser {
-
- /**
- * Used for initial separation of command word and args.
- */
- private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)");
-
- private static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)");
-
- private static final Pattern KEYWORDS_ARGS_FORMAT =
- Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace
-
- private static final Pattern PERSON_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes
- Pattern.compile("(?[^/]+)"
- + " (?p?)p/(?[^/]+)"
- + " (?p?)e/(?[^/]+)"
- + " (?p?)a/(?[^/]+)"
- + "(?(?: t/[^/]+)*)"); // variable number of tags
-
- public Parser() {}
-
- /**
- * Parses user input into command for execution.
- *
- * @param userInput full user input string
- * @return the command based on the user input
- */
- public Command parseCommand(String userInput) {
- final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim());
- if (!matcher.matches()) {
- return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE));
- }
-
- final String commandWord = matcher.group("commandWord");
- final String arguments = matcher.group("arguments");
- switch (commandWord) {
-
- case AddCommand.COMMAND_WORD:
- return prepareAdd(arguments);
-
- case SelectCommand.COMMAND_WORD:
- return prepareSelect(arguments);
-
- case DeleteCommand.COMMAND_WORD:
- return prepareDelete(arguments);
-
- case ClearCommand.COMMAND_WORD:
- return new ClearCommand();
-
- case FindCommand.COMMAND_WORD:
- return prepareFind(arguments);
-
- case ListCommand.COMMAND_WORD:
- return new ListCommand();
-
- case ExitCommand.COMMAND_WORD:
- return new ExitCommand();
-
- case HelpCommand.COMMAND_WORD:
- return new HelpCommand();
-
- default:
- return new IncorrectCommand(MESSAGE_UNKNOWN_COMMAND);
- }
- }
+public interface Parser {
/**
- * Parses arguments in the context of the add person command.
- *
- * @param args full command args string
- * @return the prepared command
+ * Parses {@code userInput} into a command and returns it.
+ * @throws ParseException if {@code userInput} does not conform the expected format
*/
- private Command prepareAdd(String args){
- final Matcher matcher = PERSON_DATA_ARGS_FORMAT.matcher(args.trim());
- // Validate arg string format
- if (!matcher.matches()) {
- return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE));
- }
- try {
- return new AddCommand(
- matcher.group("name"),
- matcher.group("phone"),
- matcher.group("email"),
- matcher.group("address"),
- getTagsFromArgs(matcher.group("tagArguments"))
- );
- } catch (IllegalValueException ive) {
- return new IncorrectCommand(ive.getMessage());
- }
- }
-
- /**
- * Extracts the new person's tags from the add command's tag arguments string.
- * Merges duplicate tag strings.
- */
- private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException {
- // no tags
- if (tagArguments.isEmpty()) {
- return Collections.emptySet();
- }
- // replace first delimiter prefix, then split
- final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/"));
- return new HashSet<>(tagStrings);
- }
-
- /**
- * Parses arguments in the context of the delete person command.
- *
- * @param args full command args string
- * @return the prepared command
- */
- private Command prepareDelete(String args) {
-
- Optional index = parseIndex(args);
- if(!index.isPresent()){
- return new IncorrectCommand(
- String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE));
- }
-
- return new DeleteCommand(index.get());
- }
-
- /**
- * Parses arguments in the context of the select person command.
- *
- * @param args full command args string
- * @return the prepared command
- */
- private Command prepareSelect(String args) {
- Optional index = parseIndex(args);
- if(!index.isPresent()){
- return new IncorrectCommand(
- String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE));
- }
-
- return new SelectCommand(index.get());
- }
-
- /**
- * Returns the specified index in the {@code command} IF a positive unsigned integer is given as the index.
- * Returns an {@code Optional.empty()} otherwise.
- */
- private Optional parseIndex(String command) {
- final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(command.trim());
- if (!matcher.matches()) {
- return Optional.empty();
- }
-
- String index = matcher.group("targetIndex");
- if(!StringUtil.isUnsignedInteger(index)){
- return Optional.empty();
- }
- return Optional.of(Integer.parseInt(index));
-
- }
-
- /**
- * Parses arguments in the context of the find person command.
- *
- * @param args full command args string
- * @return the prepared command
- */
- private Command prepareFind(String args) {
- final Matcher matcher = KEYWORDS_ARGS_FORMAT.matcher(args.trim());
- if (!matcher.matches()) {
- return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT,
- FindCommand.MESSAGE_USAGE));
- }
-
- // keywords delimited by whitespace
- final String[] keywords = matcher.group("keywords").split("\\s+");
- final Set keywordSet = new HashSet<>(Arrays.asList(keywords));
- return new FindCommand(keywordSet);
- }
-
-}
\ No newline at end of file
+ T parse(String userInput) throws ParseException;
+}
diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java
new file mode 100644
index 000000000000..65108274555c
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java
@@ -0,0 +1,100 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.commons.util.StringUtil;
+import seedu.address.model.person.Address;
+import seedu.address.model.person.DateOfBirth;
+import seedu.address.model.person.Email;
+import seedu.address.model.person.Name;
+import seedu.address.model.person.Phone;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Contains utility methods used for parsing strings in the various *Parser classes.
+ * {@code ParserUtil} contains methods that take in {@code Optional} as parameters. However, it goes against Java's
+ * convention (see https://stackoverflow.com/a/39005452) as {@code Optional} should only be used a return type.
+ * Justification: The methods in concern receive {@code Optional} return values from other methods as parameters and
+ * return {@code Optional} values based on whether the parameters were present. Therefore, it is redundant to unwrap the
+ * initial {@code Optional} before passing to {@code ParserUtil} as a parameter and then re-wrap it into an
+ * {@code Optional} return value inside {@code ParserUtil} methods.
+ */
+public class ParserUtil {
+
+ public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer.";
+ public static final String MESSAGE_INSUFFICIENT_PARTS = "Number of parts must be more than 1.";
+
+ /**
+ * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be
+ * trimmed.
+ * @throws IllegalValueException if the specified index is invalid (not non-zero unsigned integer).
+ */
+ public static Index parseIndex(String oneBasedIndex) throws IllegalValueException {
+ String trimmedIndex = oneBasedIndex.trim();
+ if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) {
+ throw new IllegalValueException(MESSAGE_INVALID_INDEX);
+ }
+ return Index.fromOneBased(Integer.parseInt(trimmedIndex));
+ }
+
+ /**
+ * Parses a {@code Optional name} into an {@code Optional} if {@code name} is present.
+ * See header comment of this class regarding the use of {@code Optional} parameters.
+ */
+ public static Optional parseName(Optional name) throws IllegalValueException {
+ requireNonNull(name);
+ return name.isPresent() ? Optional.of(new Name(name.get())) : Optional.empty();
+ }
+
+ /**
+ * Parses a {@code Optional phone} into an {@code Optional} if {@code phone} is present.
+ * See header comment of this class regarding the use of {@code Optional} parameters.
+ */
+ public static Optional parsePhone(Optional phone) throws IllegalValueException {
+ requireNonNull(phone);
+ return phone.isPresent() ? Optional.of(new Phone(phone.get())) : Optional.empty();
+ }
+
+ /**
+ * Parses a {@code Optional address} into an {@code Optional} if {@code address} is present.
+ * See header comment of this class regarding the use of {@code Optional} parameters.
+ */
+ public static Optional parseAddress(Optional address) throws IllegalValueException {
+ requireNonNull(address);
+ return address.isPresent() ? Optional.of(new Address(address.get())) : Optional.empty();
+ }
+ /**
+ * Parses a {@code Optional email} into an {@code Optional} if {@code email} is present.
+ * See header comment of this class regarding the use of {@code Optional} parameters.
+ */
+ public static Optional parseEmail(Optional email) throws IllegalValueException {
+ requireNonNull(email);
+ return email.isPresent() ? Optional.of(new Email(email.get())) : Optional.empty();
+ }
+ /**
+ * Parses a {@code Optional dob} into an {@code Optional} if {@code dob} is present.
+ * See header comment of this class regarding the use of {@code Optional} parameters.
+ */
+ public static Optional parseDateOfBirth(Optional dob) throws IllegalValueException {
+ requireNonNull(dob);
+ return dob.isPresent() ? Optional.of(new DateOfBirth(dob.get())) : Optional.empty();
+ }
+ /**
+ * Parses {@code Collection tags} into a {@code Set}.
+ */
+ public static Set parseTags(Collection tags) throws IllegalValueException {
+ requireNonNull(tags);
+ final Set tagSet = new HashSet<>();
+ for (String tagName : tags) {
+ tagSet.add(new Tag(tagName));
+ }
+ return tagSet;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java b/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java
new file mode 100644
index 000000000000..0916df545a3b
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/PartialFindCommandParser.java
@@ -0,0 +1,33 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.util.Arrays;
+
+import seedu.address.logic.commands.PartialFindCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.NameStartsWithKeywordsPredicate;
+
+/**
+ * Parses input arguments and creates a new PartialFindCommand object
+ */
+public class PartialFindCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the PartialFindCommand
+ * and returns an PartialFindCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public PartialFindCommand parse(String args) throws ParseException {
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, PartialFindCommand.MESSAGE_USAGE));
+ }
+
+ String[] nameKeywords = trimmedArgs.split("\\s+");
+
+ return new PartialFindCommand(new NameStartsWithKeywordsPredicate(Arrays.asList(nameKeywords)));
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/address/logic/parser/Prefix.java
new file mode 100644
index 000000000000..c859d5fa5db1
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/Prefix.java
@@ -0,0 +1,39 @@
+package seedu.address.logic.parser;
+
+/**
+ * A prefix that marks the beginning of an argument in an arguments string.
+ * E.g. 't/' in 'add James t/ friend'.
+ */
+public class Prefix {
+ private final String prefix;
+
+ public Prefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public String toString() {
+ return getPrefix();
+ }
+
+ @Override
+ public int hashCode() {
+ return prefix == null ? 0 : prefix.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Prefix)) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+
+ Prefix otherPrefix = (Prefix) obj;
+ return otherPrefix.getPrefix().equals(getPrefix());
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java
new file mode 100644
index 000000000000..bbcae8f4b588
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java
@@ -0,0 +1,29 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.commands.SelectCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new SelectCommand object
+ */
+public class SelectCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the SelectCommand
+ * and returns an SelectCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public SelectCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new SelectCommand(index);
+ } catch (IllegalValueException ive) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE));
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/WhyCommandParser.java b/src/main/java/seedu/address/logic/parser/WhyCommandParser.java
new file mode 100644
index 000000000000..e7b06e208016
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/WhyCommandParser.java
@@ -0,0 +1,35 @@
+package seedu.address.logic.parser;
+
+//import static java.util.Objects.requireNonNull;
+//import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
+import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.exceptions.IllegalValueException;
+//import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.commands.WhyCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * WhyCommandParser: Adapted from DeleteCommandParser due to similarities
+ */
+public class WhyCommandParser implements Parser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the ReasonCommand
+ * and returns an RemarkCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public WhyCommand parse(String args) throws ParseException {
+ /**
+ Parsing
+ */
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new WhyCommand(index);
+ } catch (IllegalValueException ive) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, WhyCommand.MESSAGE_USAGE));
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java
new file mode 100644
index 000000000000..158a1a54c1c5
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java
@@ -0,0 +1,17 @@
+package seedu.address.logic.parser.exceptions;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+
+/**
+ * Represents a parse error encountered by a parser.
+ */
+public class ParseException extends IllegalValueException {
+
+ public ParseException(String message) {
+ super(message);
+ }
+
+ public ParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java
index 298cc1b82ce8..eafc6f3b72ed 100644
--- a/src/main/java/seedu/address/model/AddressBook.java
+++ b/src/main/java/seedu/address/model/AddressBook.java
@@ -1,15 +1,23 @@
package seedu.address.model;
+import static java.util.Objects.requireNonNull;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
import javafx.collections.ObservableList;
import seedu.address.model.person.Person;
import seedu.address.model.person.ReadOnlyPerson;
import seedu.address.model.person.UniquePersonList;
+import seedu.address.model.person.exceptions.DuplicatePersonException;
+import seedu.address.model.person.exceptions.PersonNotFoundException;
import seedu.address.model.tag.Tag;
import seedu.address.model.tag.UniqueTagList;
-import java.util.*;
-import java.util.stream.Collectors;
-
/**
* Wraps all data at the address-book level
* Duplicates are not allowed (by .equals comparison)
@@ -19,6 +27,13 @@ public class AddressBook implements ReadOnlyAddressBook {
private final UniquePersonList persons;
private final UniqueTagList tags;
+ /*
+ * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication
+ * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html
+ *
+ * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication
+ * among constructors.
+ */
{
persons = new UniquePersonList();
tags = new UniqueTagList();
@@ -27,58 +42,76 @@ public class AddressBook implements ReadOnlyAddressBook {
public AddressBook() {}
/**
- * Persons and Tags are copied into this addressbook
+ * Creates an AddressBook using the Persons and Tags in the {@code toBeCopied}
*/
public AddressBook(ReadOnlyAddressBook toBeCopied) {
- this(toBeCopied.getUniquePersonList(), toBeCopied.getUniqueTagList());
- }
-
- /**
- * Persons and Tags are copied into this addressbook
- */
- public AddressBook(UniquePersonList persons, UniqueTagList tags) {
- resetData(persons.getInternalList(), tags.getInternalList());
- }
-
- public static ReadOnlyAddressBook getEmptyAddressBook() {
- return new AddressBook();
+ this();
+ resetData(toBeCopied);
}
-//// list overwrite operations
+ //// list overwrite operations
- public ObservableList getPersons() {
- return persons.getInternalList();
+ public void setPersons(List extends ReadOnlyPerson> persons) throws DuplicatePersonException {
+ this.persons.setPersons(persons);
}
- public void setPersons(List persons) {
- this.persons.getInternalList().setAll(persons);
- }
-
- public void setTags(Collection tags) {
- this.tags.getInternalList().setAll(tags);
- }
-
- public void resetData(Collection extends ReadOnlyPerson> newPersons, Collection newTags) {
- setPersons(newPersons.stream().map(Person::new).collect(Collectors.toList()));
- setTags(newTags);
+ public void setTags(Set tags) {
+ this.tags.setTags(tags);
}
+ /**
+ * Resets the existing data of this {@code AddressBook} with {@code newData}.
+ */
public void resetData(ReadOnlyAddressBook newData) {
- resetData(newData.getPersonList(), newData.getTagList());
+ requireNonNull(newData);
+ try {
+ setPersons(newData.getPersonList());
+ } catch (DuplicatePersonException e) {
+ assert false : "AddressBooks should not have duplicate persons";
+ }
+
+ setTags(new HashSet<>(newData.getTagList()));
+ syncMasterTagListWith(persons);
}
-//// person-level operations
+ //// person-level operations
/**
* Adds a person to the address book.
* Also checks the new person's tags and updates {@link #tags} with any new tags found,
* and updates the Tag objects in the person to point to those in {@link #tags}.
*
- * @throws UniquePersonList.DuplicatePersonException if an equivalent person already exists.
+ * @throws DuplicatePersonException if an equivalent person already exists.
*/
- public void addPerson(Person p) throws UniquePersonList.DuplicatePersonException {
- syncTagsWithMasterList(p);
- persons.add(p);
+ public void addPerson(ReadOnlyPerson p) throws DuplicatePersonException {
+ Person newPerson = new Person(p);
+ syncMasterTagListWith(newPerson);
+ // TODO: the tags master list will be updated even though the below line fails.
+ // This can cause the tags master list to have additional tags that are not tagged to any person
+ // in the person list.
+ persons.add(newPerson);
+ }
+
+ /**
+ * Replaces the given person {@code target} in the list with {@code editedReadOnlyPerson}.
+ * {@code AddressBook}'s tag list will be updated with the tags of {@code editedReadOnlyPerson}.
+ *
+ * @throws DuplicatePersonException if updating the person's details causes the person to be equivalent to
+ * another existing person in the list.
+ * @throws PersonNotFoundException if {@code target} could not be found in the list.
+ *
+ * @see #syncMasterTagListWith(Person)
+ */
+ public void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedReadOnlyPerson)
+ throws DuplicatePersonException, PersonNotFoundException {
+ requireNonNull(editedReadOnlyPerson);
+
+ Person editedPerson = new Person(editedReadOnlyPerson);
+ syncMasterTagListWith(editedPerson);
+ // TODO: the tags master list will be updated even though the below line fails.
+ // This can cause the tags master list to have additional tags that are not tagged to any person
+ // in the person list.
+ persons.setPerson(target, editedPerson);
}
/**
@@ -86,73 +119,73 @@ public void addPerson(Person p) throws UniquePersonList.DuplicatePersonException
* - exists in the master list {@link #tags}
* - points to a Tag object in the master list
*/
- private void syncTagsWithMasterList(Person person) {
- final UniqueTagList personTags = person.getTags();
+ private void syncMasterTagListWith(Person person) {
+ final UniqueTagList personTags = new UniqueTagList(person.getTags());
tags.mergeFrom(personTags);
// Create map with values = tag object references in the master list
+ // used for checking person tag references
final Map masterTagObjects = new HashMap<>();
- for (Tag tag : tags) {
- masterTagObjects.put(tag, tag);
- }
+ tags.forEach(tag -> masterTagObjects.put(tag, tag));
- // Rebuild the list of person tags using references from the master list
- final Set commonTagReferences = new HashSet<>();
- for (Tag tag : personTags) {
- commonTagReferences.add(masterTagObjects.get(tag));
- }
- person.setTags(new UniqueTagList(commonTagReferences));
+ // Rebuild the list of person tags to point to the relevant tags in the master tag list.
+ final Set correctTagReferences = new HashSet<>();
+ personTags.forEach(tag -> correctTagReferences.add(masterTagObjects.get(tag)));
+ person.setTags(correctTagReferences);
}
- public boolean removePerson(ReadOnlyPerson key) throws UniquePersonList.PersonNotFoundException {
+ /**
+ * Ensures that every tag in these persons:
+ * - exists in the master list {@link #tags}
+ * - points to a Tag object in the master list
+ * @see #syncMasterTagListWith(Person)
+ */
+ private void syncMasterTagListWith(UniquePersonList persons) {
+ persons.forEach(this::syncMasterTagListWith);
+ }
+
+ /**
+ * Removes {@code key} from this {@code AddressBook}.
+ * @throws PersonNotFoundException if the {@code key} is not in this {@code AddressBook}.
+ */
+ public boolean removePerson(ReadOnlyPerson key) throws PersonNotFoundException {
if (persons.remove(key)) {
return true;
} else {
- throw new UniquePersonList.PersonNotFoundException();
+ throw new PersonNotFoundException();
}
}
-//// tag-level operations
+ //// tag-level operations
public void addTag(Tag t) throws UniqueTagList.DuplicateTagException {
tags.add(t);
}
-//// util methods
+ //// util methods
@Override
public String toString() {
- return persons.getInternalList().size() + " persons, " + tags.getInternalList().size() + " tags";
+ return persons.asObservableList().size() + " persons, " + tags.asObservableList().size() + " tags";
// TODO: refine later
}
@Override
- public List getPersonList() {
- return Collections.unmodifiableList(persons.getInternalList());
- }
-
- @Override
- public List getTagList() {
- return Collections.unmodifiableList(tags.getInternalList());
+ public ObservableList getPersonList() {
+ return persons.asObservableList();
}
@Override
- public UniquePersonList getUniquePersonList() {
- return this.persons;
+ public ObservableList getTagList() {
+ return tags.asObservableList();
}
- @Override
- public UniqueTagList getUniqueTagList() {
- return this.tags;
- }
-
-
@Override
public boolean equals(Object other) {
return other == this // short circuit if same object
|| (other instanceof AddressBook // instanceof handles nulls
&& this.persons.equals(((AddressBook) other).persons)
- && this.tags.equals(((AddressBook) other).tags));
+ && this.tags.equalsOrderInsensitive(((AddressBook) other).tags));
}
@Override
diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java
index d14a27a93b5e..9c6aa4e766a0 100644
--- a/src/main/java/seedu/address/model/Model.java
+++ b/src/main/java/seedu/address/model/Model.java
@@ -1,16 +1,19 @@
package seedu.address.model;
-import seedu.address.commons.core.UnmodifiableObservableList;
-import seedu.address.model.person.Person;
-import seedu.address.model.person.ReadOnlyPerson;
-import seedu.address.model.person.UniquePersonList;
+import java.util.function.Predicate;
-import java.util.Set;
+import javafx.collections.ObservableList;
+import seedu.address.model.person.ReadOnlyPerson;
+import seedu.address.model.person.exceptions.DuplicatePersonException;
+import seedu.address.model.person.exceptions.PersonNotFoundException;
/**
* The API of the Model component.
*/
public interface Model {
+ /** {@code Predicate} that always evaluate to true */
+ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true;
+
/** Clears existing backing model and replaces with the provided new data. */
void resetData(ReadOnlyAddressBook newData);
@@ -18,18 +21,28 @@ public interface Model {
ReadOnlyAddressBook getAddressBook();
/** Deletes the given person. */
- void deletePerson(ReadOnlyPerson target) throws UniquePersonList.PersonNotFoundException;
+ void deletePerson(ReadOnlyPerson target) throws PersonNotFoundException;
/** Adds the given person */
- void addPerson(Person person) throws UniquePersonList.DuplicatePersonException;
-
- /** Returns the filtered person list as an {@code UnmodifiableObservableList} */
- UnmodifiableObservableList getFilteredPersonList();
-
- /** Updates the filter of the filtered person list to show all persons */
- void updateFilteredListToShowAll();
-
- /** Updates the filter of the filtered person list to filter by the given keywords*/
- void updateFilteredPersonList(Set keywords);
+ void addPerson(ReadOnlyPerson person) throws DuplicatePersonException;
+
+ /**
+ * Replaces the given person {@code target} with {@code editedPerson}.
+ *
+ * @throws DuplicatePersonException if updating the person's details causes the person to be equivalent to
+ * another existing person in the list.
+ * @throws PersonNotFoundException if {@code target} could not be found in the list.
+ */
+ void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson)
+ throws DuplicatePersonException, PersonNotFoundException;
+
+ /** Returns an unmodifiable view of the filtered person list */
+ ObservableList getFilteredPersonList();
+
+ /**
+ * Updates the filter of the filtered person list to filter by the given {@code predicate}.
+ * @throws NullPointerException if {@code predicate} is null.
+ */
+ void updateFilteredPersonList(Predicate predicate);
}
diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java
index 869226d02bf1..095c831cfbf8 100644
--- a/src/main/java/seedu/address/model/ModelManager.java
+++ b/src/main/java/seedu/address/model/ModelManager.java
@@ -1,18 +1,20 @@
package seedu.address.model;
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
+import seedu.address.commons.core.ComponentManager;
import seedu.address.commons.core.LogsCenter;
-import seedu.address.commons.core.UnmodifiableObservableList;
-import seedu.address.commons.util.StringUtil;
import seedu.address.commons.events.model.AddressBookChangedEvent;
-import seedu.address.commons.core.ComponentManager;
-import seedu.address.model.person.Person;
import seedu.address.model.person.ReadOnlyPerson;
-import seedu.address.model.person.UniquePersonList;
-import seedu.address.model.person.UniquePersonList.PersonNotFoundException;
-
-import java.util.Set;
-import java.util.logging.Logger;
+import seedu.address.model.person.exceptions.DuplicatePersonException;
+import seedu.address.model.person.exceptions.PersonNotFoundException;
/**
* Represents the in-memory model of the address book data.
@@ -22,32 +24,25 @@ public class ModelManager extends ComponentManager implements Model {
private static final Logger logger = LogsCenter.getLogger(ModelManager.class);
private final AddressBook addressBook;
- private final FilteredList filteredPersons;
+ private final FilteredList filteredPersons;
/**
- * Initializes a ModelManager with the given AddressBook
- * AddressBook and its variables should not be null
+ * Initializes a ModelManager with the given addressBook and userPrefs.
*/
- public ModelManager(AddressBook src, UserPrefs userPrefs) {
+ public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) {
super();
- assert src != null;
- assert userPrefs != null;
+ requireAllNonNull(addressBook, userPrefs);
- logger.fine("Initializing with address book: " + src + " and user prefs " + userPrefs);
+ logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs);
- addressBook = new AddressBook(src);
- filteredPersons = new FilteredList<>(addressBook.getPersons());
+ this.addressBook = new AddressBook(addressBook);
+ filteredPersons = new FilteredList<>(this.addressBook.getPersonList());
}
public ModelManager() {
this(new AddressBook(), new UserPrefs());
}
- public ModelManager(ReadOnlyAddressBook initialData, UserPrefs userPrefs) {
- addressBook = new AddressBook(initialData);
- filteredPersons = new FilteredList<>(addressBook.getPersons());
- }
-
@Override
public void resetData(ReadOnlyAddressBook newData) {
addressBook.resetData(newData);
@@ -71,83 +66,54 @@ public synchronized void deletePerson(ReadOnlyPerson target) throws PersonNotFou
}
@Override
- public synchronized void addPerson(Person person) throws UniquePersonList.DuplicatePersonException {
+ public synchronized void addPerson(ReadOnlyPerson person) throws DuplicatePersonException {
addressBook.addPerson(person);
- updateFilteredListToShowAll();
+ updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
indicateAddressBookChanged();
}
- //=========== Filtered Person List Accessors ===============================================================
-
- @Override
- public UnmodifiableObservableList getFilteredPersonList() {
- return new UnmodifiableObservableList<>(filteredPersons);
- }
-
@Override
- public void updateFilteredListToShowAll() {
- filteredPersons.setPredicate(null);
- }
-
- @Override
- public void updateFilteredPersonList(Set keywords){
- updateFilteredPersonList(new PredicateExpression(new NameQualifier(keywords)));
- }
+ public void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson)
+ throws DuplicatePersonException, PersonNotFoundException {
+ requireAllNonNull(target, editedPerson);
- private void updateFilteredPersonList(Expression expression) {
- filteredPersons.setPredicate(expression::satisfies);
- }
-
- //========== Inner classes/interfaces used for filtering ==================================================
-
- interface Expression {
- boolean satisfies(ReadOnlyPerson person);
- String toString();
+ addressBook.updatePerson(target, editedPerson);
+ indicateAddressBookChanged();
}
- private class PredicateExpression implements Expression {
-
- private final Qualifier qualifier;
-
- PredicateExpression(Qualifier qualifier) {
- this.qualifier = qualifier;
- }
-
- @Override
- public boolean satisfies(ReadOnlyPerson person) {
- return qualifier.run(person);
- }
+ //=========== Filtered Person List Accessors =============================================================
- @Override
- public String toString() {
- return qualifier.toString();
- }
+ /**
+ * Returns an unmodifiable view of the list of {@code ReadOnlyPerson} backed by the internal list of
+ * {@code addressBook}
+ */
+ @Override
+ public ObservableList getFilteredPersonList() {
+ return FXCollections.unmodifiableObservableList(filteredPersons);
}
- interface Qualifier {
- boolean run(ReadOnlyPerson person);
- String toString();
+ @Override
+ public void updateFilteredPersonList(Predicate predicate) {
+ requireNonNull(predicate);
+ filteredPersons.setPredicate(predicate);
}
- private class NameQualifier implements Qualifier {
- private Set nameKeyWords;
-
- NameQualifier(Set nameKeyWords) {
- this.nameKeyWords = nameKeyWords;
+ @Override
+ public boolean equals(Object obj) {
+ // short circuit if same object
+ if (obj == this) {
+ return true;
}
- @Override
- public boolean run(ReadOnlyPerson person) {
- return nameKeyWords.stream()
- .filter(keyword -> StringUtil.containsIgnoreCase(person.getName().fullName, keyword))
- .findAny()
- .isPresent();
+ // instanceof handles nulls
+ if (!(obj instanceof ModelManager)) {
+ return false;
}
- @Override
- public String toString() {
- return "name=" + String.join(", ", nameKeyWords);
- }
+ // state check
+ ModelManager other = (ModelManager) obj;
+ return addressBook.equals(other.addressBook)
+ && filteredPersons.equals(other.filteredPersons);
}
}
diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java
index bfca099b1e81..df24c7d9e928 100644
--- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java
+++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java
@@ -1,30 +1,24 @@
package seedu.address.model;
-
+import javafx.collections.ObservableList;
import seedu.address.model.person.ReadOnlyPerson;
-import seedu.address.model.person.UniquePersonList;
import seedu.address.model.tag.Tag;
-import seedu.address.model.tag.UniqueTagList;
-
-import java.util.List;
/**
* Unmodifiable view of an address book
*/
public interface ReadOnlyAddressBook {
- UniqueTagList getUniqueTagList();
-
- UniquePersonList getUniquePersonList();
-
/**
- * Returns an unmodifiable view of persons list
+ * Returns an unmodifiable view of the persons list.
+ * This list will not contain any duplicate persons.
*/
- List getPersonList();
+ ObservableList getPersonList();
/**
- * Returns an unmodifiable view of tags list
+ * Returns an unmodifiable view of the tags list.
+ * This list will not contain any duplicate tags.
*/
- List getTagList();
+ ObservableList getTagList();
}
diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java
index da9c8037f495..8c8a071876eb 100644
--- a/src/main/java/seedu/address/model/UserPrefs.java
+++ b/src/main/java/seedu/address/model/UserPrefs.java
@@ -1,15 +1,21 @@
package seedu.address.model;
-import seedu.address.commons.core.GuiSettings;
-
import java.util.Objects;
+import seedu.address.commons.core.GuiSettings;
+
/**
* Represents User's preferences.
*/
public class UserPrefs {
- public GuiSettings guiSettings;
+ private GuiSettings guiSettings;
+ private String addressBookFilePath = "data/addressbook.xml";
+ private String addressBookName = "MyAddressBook";
+
+ public UserPrefs() {
+ this.setGuiSettings(500, 500, 0, 0);
+ }
public GuiSettings getGuiSettings() {
return guiSettings == null ? new GuiSettings() : guiSettings;
@@ -19,36 +25,54 @@ public void updateLastUsedGuiSetting(GuiSettings guiSettings) {
this.guiSettings = guiSettings;
}
- public UserPrefs(){
- this.setGuiSettings(500, 500, 0, 0);
- }
-
public void setGuiSettings(double width, double height, int x, int y) {
guiSettings = new GuiSettings(width, height, x, y);
}
+ public String getAddressBookFilePath() {
+ return addressBookFilePath;
+ }
+
+ public void setAddressBookFilePath(String addressBookFilePath) {
+ this.addressBookFilePath = addressBookFilePath;
+ }
+
+ public String getAddressBookName() {
+ return addressBookName;
+ }
+
+ public void setAddressBookName(String addressBookName) {
+ this.addressBookName = addressBookName;
+ }
+
@Override
public boolean equals(Object other) {
- if (other == this){
+ if (other == this) {
return true;
}
- if (!(other instanceof UserPrefs)){ //this handles null as well.
+ if (!(other instanceof UserPrefs)) { //this handles null as well.
return false;
}
- UserPrefs o = (UserPrefs)other;
+ UserPrefs o = (UserPrefs) other;
- return Objects.equals(guiSettings, o.guiSettings);
+ return Objects.equals(guiSettings, o.guiSettings)
+ && Objects.equals(addressBookFilePath, o.addressBookFilePath)
+ && Objects.equals(addressBookName, o.addressBookName);
}
@Override
public int hashCode() {
- return Objects.hash(guiSettings);
+ return Objects.hash(guiSettings, addressBookFilePath, addressBookName);
}
@Override
- public String toString(){
- return guiSettings.toString();
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Gui Settings : " + guiSettings.toString());
+ sb.append("\nLocal data file location : " + addressBookFilePath);
+ sb.append("\nAddressBook name : " + addressBookName);
+ return sb.toString();
}
}
diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java
index a2bd109c005e..f284e1373ac9 100644
--- a/src/main/java/seedu/address/model/person/Address.java
+++ b/src/main/java/seedu/address/model/person/Address.java
@@ -1,5 +1,6 @@
package seedu.address.model.person;
+import static java.util.Objects.requireNonNull;
import seedu.address.commons.exceptions.IllegalValueException;
@@ -8,19 +9,33 @@
* Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)}
*/
public class Address {
-
- public static final String MESSAGE_ADDRESS_CONSTRAINTS = "Person addresses can be in any format";
- public static final String ADDRESS_VALIDATION_REGEX = ".+";
+
+ public static final String MESSAGE_ADDRESS_CONSTRAINTS =
+ "Person addresses can take any values, and it should not be blank";
+
+ /*
+ * The first character of the address must not be a whitespace,
+ * otherwise " " (a blank string) becomes a valid input.
+ */
+ public static final String ADDRESS_VALIDATION_REGEX = "[^\\s].*";
public final String value;
+ /**
+ * Initialise a Address object with value of empty String. This can ONLY be used in the default field of
+ * {@code AddPersonOptionalFieldDescriptor}
+ */
+ public Address() {
+ this.value = "";
+ }
+
/**
* Validates given address.
*
* @throws IllegalValueException if given address string is invalid.
*/
public Address(String address) throws IllegalValueException {
- assert address != null;
+ requireNonNull(address);
if (!isValidAddress(address)) {
throw new IllegalValueException(MESSAGE_ADDRESS_CONSTRAINTS);
}
@@ -51,4 +66,4 @@ public int hashCode() {
return value.hashCode();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/seedu/address/model/person/DateOfBirth.java b/src/main/java/seedu/address/model/person/DateOfBirth.java
new file mode 100644
index 000000000000..040e1cd87ecb
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/DateOfBirth.java
@@ -0,0 +1,66 @@
+package seedu.address.model.person;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+
+/**
+ * Represents a Person's date of birth in the address book.
+ */
+public class DateOfBirth {
+
+ public static final String MESSAGE_DOB_CONSTRAINTS =
+ "Person's date of birth should only contain numeric characters and spaces, and it should not be blank";
+
+ /*
+ * The first character of the address must not be a whitespace,
+ * otherwise " " (a blank string) becomes a valid input.
+ */
+ public static final String DOB_VALIDATION_REGEX = "[0-9][0-9]\\s+[0-9][0-9]\\s+[0-9][0-9][0-9][0-9].*";
+
+ public final String finalDateOfBirth;
+
+ /**
+ * Initialise a DateOfBirth object with value of empty String. This can ONLY be used in the default field of
+ * {@code AddPersonOptionalFieldDescriptor}
+ */
+ public DateOfBirth() {
+ this.finalDateOfBirth = "";
+ }
+
+ /**
+ * Validates given Date of Birth.
+ *
+ * @throws IllegalValueException if given date of birth string is invalid.
+ */
+ public DateOfBirth(String dob) throws IllegalValueException {
+ requireNonNull(dob);
+ if (!isValidDateOfBirth(dob)) {
+ throw new IllegalValueException(MESSAGE_DOB_CONSTRAINTS);
+ }
+ this.finalDateOfBirth = dob;
+ }
+
+ /**
+ * Returns true if a given string is a valid person date of birth.
+ */
+ public static boolean isValidDateOfBirth(String test) {
+ return test.matches(DOB_VALIDATION_REGEX);
+ }
+ @Override
+ public String toString() {
+ return finalDateOfBirth;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof DateOfBirth // instanceof handles nulls
+ && this.finalDateOfBirth.equals(((DateOfBirth) other).finalDateOfBirth)); // state check
+ }
+
+ @Override
+ public int hashCode() {
+ return finalDateOfBirth.hashCode();
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java
index 5da4d1078236..13572b176466 100644
--- a/src/main/java/seedu/address/model/person/Email.java
+++ b/src/main/java/seedu/address/model/person/Email.java
@@ -1,5 +1,6 @@
package seedu.address.model.person;
+import static java.util.Objects.requireNonNull;
import seedu.address.commons.exceptions.IllegalValueException;
@@ -15,18 +16,26 @@ public class Email {
public final String value;
+ /**
+ * Initialise a Email object with value of empty String. This can ONLY be used in the default field of
+ * {@code AddPersonOptionalFieldDescriptor}
+ */
+ public Email() {
+ this.value = "";
+ }
+
/**
* Validates given email.
*
* @throws IllegalValueException if given email address string is invalid.
*/
public Email(String email) throws IllegalValueException {
- assert email != null;
- email = email.trim();
- if (!isValidEmail(email)) {
+ requireNonNull(email);
+ String trimmedEmail = email.trim();
+ if (!isValidEmail(trimmedEmail)) {
throw new IllegalValueException(MESSAGE_EMAIL_CONSTRAINTS);
}
- this.value = email;
+ this.value = trimmedEmail;
}
/**
diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java
index 4f30033e70fe..5d138e8ba223 100644
--- a/src/main/java/seedu/address/model/person/Name.java
+++ b/src/main/java/seedu/address/model/person/Name.java
@@ -1,5 +1,7 @@
package seedu.address.model.person;
+import static java.util.Objects.requireNonNull;
+
import seedu.address.commons.exceptions.IllegalValueException;
/**
@@ -8,8 +10,14 @@
*/
public class Name {
- public static final String MESSAGE_NAME_CONSTRAINTS = "Person names should be spaces or alphanumeric characters";
- public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum} ]+";
+ public static final String MESSAGE_NAME_CONSTRAINTS =
+ "Person names should only contain alphanumeric characters and spaces, and it should not be blank";
+
+ /*
+ * The first character of the address must not be a whitespace,
+ * otherwise " " (a blank string) becomes a valid input.
+ */
+ public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*";
public final String fullName;
@@ -19,12 +27,12 @@ public class Name {
* @throws IllegalValueException if given name string is invalid.
*/
public Name(String name) throws IllegalValueException {
- assert name != null;
- name = name.trim();
- if (!isValidName(name)) {
+ requireNonNull(name);
+ String trimmedName = name.trim();
+ if (!isValidName(trimmedName)) {
throw new IllegalValueException(MESSAGE_NAME_CONSTRAINTS);
}
- this.fullName = name;
+ this.fullName = trimmedName;
}
/**
diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java
new file mode 100644
index 000000000000..9d73fd178b0f
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java
@@ -0,0 +1,31 @@
+package seedu.address.model.person;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.StringUtil;
+
+/**
+ * Tests that a {@code ReadOnlyPerson}'s {@code Name} matches any of the keywords given.
+ */
+public class NameContainsKeywordsPredicate implements Predicate {
+ private final List keywords;
+
+ public NameContainsKeywordsPredicate(List keywords) {
+ this.keywords = keywords;
+ }
+
+ @Override
+ public boolean test(ReadOnlyPerson person) {
+ return keywords.stream()
+ .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls
+ && this.keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check
+ }
+
+}
diff --git a/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java
new file mode 100644
index 000000000000..38c64de4684e
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/NameStartsWithKeywordsPredicate.java
@@ -0,0 +1,29 @@
+package seedu.address.model.person;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Tests that a {@code ReadOnlyPerson}'s {@code Name} matches any of the keywords given.
+ */
+public class NameStartsWithKeywordsPredicate implements Predicate {
+ private final List keywords;
+
+ public NameStartsWithKeywordsPredicate(List keywords) {
+ this.keywords = keywords;
+ }
+
+ @Override
+ public boolean test(ReadOnlyPerson person) {
+ return keywords.stream()
+ .anyMatch(keyword -> person.getName().fullName.toLowerCase().startsWith(keyword.toLowerCase()));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this // short circuit if same object
+ || (other instanceof NameStartsWithKeywordsPredicate // instanceof handles nulls
+ && this.keywords.equals(((NameStartsWithKeywordsPredicate) other).keywords)); // state check
+ }
+
+}
diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java
index 03ffce7d2e79..8725c9991721 100644
--- a/src/main/java/seedu/address/model/person/Person.java
+++ b/src/main/java/seedu/address/model/person/Person.java
@@ -1,9 +1,16 @@
package seedu.address.model.person;
-import seedu.address.commons.util.CollectionUtil;
-import seedu.address.model.tag.UniqueTagList;
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import static seedu.address.logic.commands.WhyCommand.SHOWING_WHY_MESSAGE;
import java.util.Objects;
+import java.util.Set;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import seedu.address.model.tag.Tag;
+import seedu.address.model.tag.UniqueTagList;
/**
* Represents a Person in the address book.
@@ -11,62 +18,134 @@
*/
public class Person implements ReadOnlyPerson {
- private Name name;
- private Phone phone;
- private Email email;
- private Address address;
+ private ObjectProperty name;
+ private ObjectProperty phone;
+ private ObjectProperty email;
+ private ObjectProperty address;
+ private ObjectProperty dob;
- private UniqueTagList tags;
+ private String reason;
+
+ private ObjectProperty tags;
/**
* Every field must be present and not null.
*/
- public Person(Name name, Phone phone, Email email, Address address, UniqueTagList tags) {
- assert !CollectionUtil.isAnyNull(name, phone, email, address, tags);
- this.name = name;
- this.phone = phone;
- this.email = email;
- this.address = address;
- this.tags = new UniqueTagList(tags); // protect internal tags from changes in the arg list
+ public Person(Name name, Phone phone, Email email, Address address, DateOfBirth dob, Set