diff --git a/.travis.yml b/.travis.yml index 4d6f37d8ba8b..73f3194bcc0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ matrix: script: >- ./config/travis/run-checks.sh && - travis_retry ./gradlew clean checkstyleMain checkstyleTest headless allTests coverage coveralls asciidoctor copyDummySearchPage + travis_retry ./gradlew clean checkstyleMain checkstyleTest headless allTests coverage coveralls asciidoctor copyDummySearchPage copyLoadingPage deploy: skip_cleanup: true diff --git a/README.adoc b/README.adoc index 03eff3a4d191..c4a30e48eefd 100644 --- a/README.adoc +++ b/README.adoc @@ -1,10 +1,9 @@ -= Address Book (Level 4) += Media Socializer ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] +https://travis-ci.org/CS2103JAN2018-F12-B3/main[image:https://travis-ci.org/CS2103JAN2018-F12-B3/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/shadow2496/main[image:https://ci.appveyor.com/api/projects/status/qe3vke3edgqhmxsj?svg=true[Build status]] +https://coveralls.io/github/CS2103JAN2018-F12-B3/main?branch=master[image:https://coveralls.io/repos/github/CS2103JAN2018-F12-B3/main/badge.svg?branch=master[Coverage Status]] https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] ifdef::env-github[] @@ -15,19 +14,19 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +== For All Your Social Media Needs + +Media Socializer aims to provide you with one-stop access to your favourite social media platforms. +It keeps track of the various platforms your friends are using and sends you straight to their page if you so desire. +You can also store them as contacts in case you need to communicate with them offline. +Check out the social feeds of different people on multiple platforms all in a single window! + +*Go on, grab the app and get started now!* == Site Map * <> * <> -* <> * <> * <> @@ -35,6 +34,7 @@ endif::[] * Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. +* The AddressBook-Level4 project was created by the https://github.com/se-edu/[SE-EDU] initiative. * Libraries used: https://github.com/TomasMikula/EasyBind[EasyBind], https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit4[JUnit4] == Licence : link:LICENSE[MIT] diff --git a/build.gradle b/build.gradle index 50cd2ae52efc..6d4ce89ccbe5 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' compile group: 'com.google.guava', name: 'guava', version: '19.0' + compile group: 'com.restfb', name: 'restfb', version: '2.3.0' testCompile group: 'junit', name: 'junit', version: '4.12' testCompile group: 'org.testfx', name: 'testfx-core', version: testFxVersion @@ -57,7 +58,7 @@ dependencies { } shadowJar { - archiveName = "addressbook.jar" + archiveName = "mediasocializer.jar" destinationDir = file("${buildDir}/jar/") } @@ -201,6 +202,11 @@ task copyDummySearchPage(type: Copy) { into "${buildDir}/docs/html5" } +task copyLoadingPage(type: Copy) { + from 'docs/LoadingPage.html' + into "${buildDir}/docs/html5" +} + deployOfflineDocs.dependsOn asciidoctor processResources.dependsOn deployOfflineDocs diff --git a/collated/functional/KevinChuangCH.md b/collated/functional/KevinChuangCH.md new file mode 100644 index 000000000000..66f3b3371705 --- /dev/null +++ b/collated/functional/KevinChuangCH.md @@ -0,0 +1,340 @@ +# KevinChuangCH +###### \java\seedu\address\commons\events\ui\SearchPersonEvent.java +``` java +/** + * Represents a search for person. + */ +public class SearchPersonEvent extends BaseEvent { + + private final String searchName; + private final String platformToSearch; + + public SearchPersonEvent(String platformToSearch, String searchName) { + this.searchName = searchName; + this.platformToSearch = platformToSearch; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + public String getSearchName() { + return searchName; + } + + public String getPlatform() { + return platformToSearch; + } + +} +``` +###### \java\seedu\address\logic\commands\FindWithTagCommand.java +``` java +/** + * Finds and lists all persons in address book whose tags contain any of the argument keywords. + * Keyword matching is case sensitive. + */ +public class FindWithTagCommand extends Command { + + public static final String COMMAND_WORD = "findtag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose tags 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 + " friends owesMoney"; + + private final TagContainsKeywordsPredicate predicate; + + public FindWithTagCommand(TagContainsKeywordsPredicate 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 FindWithTagCommand // instanceof handles nulls + && this.predicate.equals(((FindWithTagCommand) other).predicate)); // state check + } +} +``` +###### \java\seedu\address\logic\commands\SearchCommand.java +``` java +/** + * Search for a person with the input name either on all available social media platforms + * or the stated social media platform if it is available.. + */ +public class SearchCommand extends Command { + + public static final String COMMAND_WORD = "search"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Search for a person with the input name on the input social media platform if it is available.\n" + + "If no platform is stated, then the search will be performed on all available social media platforms.\n" + + "User can use alias to indicate the social media platform.\n" + + "Parameters: [PLATFORM], NAME (PLATFORM is case sensitive and NAME should not be blank)\n" + + "Example: " + COMMAND_WORD + " " + Facebook.PLATFORM_KEYWORD + ", Tom\n" + + "or\n" + + "Example: " + COMMAND_WORD + " " + Twitter.PLATFORM_ALIAS + ", Sam\n" + + "or\n" + + "Example: " + COMMAND_WORD + " Jason\n"; + + public static final String MESSAGE_SEARCH_PERSON_SUCCESS = "Searched Person: %1$s"; + + private final String targetPlatform; + private final String targetName; + + public SearchCommand(String targetPlatform, String targetName) { + this.targetName = targetName; + this.targetPlatform = targetPlatform; + } + + @Override + public CommandResult execute() throws CommandException { + + EventsCenter.getInstance().post(new SearchPersonEvent(targetPlatform, targetName)); + return new CommandResult(String.format(MESSAGE_SEARCH_PERSON_SUCCESS, targetName)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SearchCommand // instanceof handles nulls + && this.targetName.equals(((SearchCommand) other).targetName)); // state check + } +} +``` +###### \java\seedu\address\logic\parser\FindWithTagCommandParser.java +``` java +/** + * Parses input arguments and creates a new FindWithTagCommand object + */ +public class FindWithTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindWithTagCommand + * and returns an FindWithTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindWithTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindWithTagCommand.MESSAGE_USAGE)); + } + + String[] tagKeywords = trimmedArgs.split("\\s+"); + + return new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList(tagKeywords))); + } + +} +``` +###### \java\seedu\address\logic\parser\ParserUtil.java +``` java + /** + * Checks for the validation of {@code inputPlatform} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified name is invalid (not following the name regex). + */ + public static String parsePlatformToSearch(String inputPlatform) throws IllegalValueException { + requireNonNull(inputPlatform); + String trimmedInputPlatform = inputPlatform.trim(); + if (!SocialMediaPlatform.isValidPlatform(trimmedInputPlatform)) { + throw new IllegalValueException(SocialMediaPlatform.MESSAGE_PLATFORM_CONSTRAINTS); + } + return trimmedInputPlatform; + } + + /** + * Checks for the validation of {@code inputName} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified name is invalid (not following the name regex). + */ + public static String parseSearchName(String inputName) throws IllegalValueException { + requireNonNull(inputName); + String trimmedInputName = inputName.trim(); + if (!trimmedInputName.matches(SocialMediaPlatform.USERNAME_VALIDATION_REGEX)) { + throw new IllegalValueException(SocialMediaPlatform.MESSAGE_USERNAME_CONSTRAINTS); + } + return trimmedInputName; + } +``` +###### \java\seedu\address\logic\parser\SearchCommandParser.java +``` java +/** + * Parses input arguments and creates a new SearchCommand object + */ +public class SearchCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SearchCommand + * and returns an SearchCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SearchCommand parse(String args) throws ParseException { + String[] splitArgs = args.split(", "); + if (splitArgs.length == 1) { + String platform = "all"; + try { + String inputName = ParserUtil.parseSearchName(splitArgs[0]); + return new SearchCommand(platform, inputName); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } else if (splitArgs.length == 2) { + try { + String platform = ParserUtil.parsePlatformToSearch(splitArgs[0]); + String inputName = ParserUtil.parseSearchName(splitArgs[1]); + return new SearchCommand(platform, inputName); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } +} +``` +###### \java\seedu\address\model\person\Person.java +``` java + /** + * Returns the tag names of tags of a person as String. + * @return a string of all the tag names of tags of a person. + */ + public String getTagsAsString() { + final StringBuilder builder = new StringBuilder(); + for (String tag : tags.arrayOfTags()) { + builder.append(tag); + builder.append(" "); + } + return builder.toString(); + } + +} +``` +###### \java\seedu\address\model\person\TagContainsKeywordsPredicate.java +``` java +/** + * Tests that a {@code Person}'s {@code Tags} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getTagsAsString().trim(), keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } +} +``` +###### \java\seedu\address\model\smplatform\SocialMediaPlatform.java +``` java +/** + * Represents a social media platform, which can take many forms. + */ +public abstract class SocialMediaPlatform { + + public static final String MESSAGE_USERNAME_CONSTRAINTS = + "Social media usernames should only contain alphanumeric characters, " + + "underscore, and spaces, and it should not be blank"; + + public static final String USERNAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum}_ ]*"; + + public static final String MESSAGE_PLATFORM_CONSTRAINTS = + "Platforms available are : \n" + + "1) " + Facebook.PLATFORM_KEYWORD + " (alias: " + Facebook.PLATFORM_ALIAS + ")\n" + + "2) " + Twitter.PLATFORM_KEYWORD + " (alias: " + Twitter.PLATFORM_ALIAS + ")\n"; + +``` +###### \java\seedu\address\model\smplatform\SocialMediaPlatform.java +``` java + /** + * Returns true if a given string is a valid social platform name. + */ + public static boolean isValidPlatform(String test) { + if (test.equals(Facebook.PLATFORM_KEYWORD) + || test.equals(Facebook.PLATFORM_ALIAS) + || test.equals(Twitter.PLATFORM_KEYWORD) + || test.equals(Twitter.PLATFORM_ALIAS)) { + return true; + } + return false; + } +} +``` +###### \java\seedu\address\model\tag\UniqueTagList.java +``` java + /** + * Returns an array of tag names of tags of a person. + * @return array of tag names + */ + public ArrayList arrayOfTags() { + assert CollectionUtil.elementsAreUnique(internalList); + ArrayList tagStringArray = new ArrayList<>(); + for (Tag t : internalList) { + tagStringArray.add(t.tagName); + } + return tagStringArray; + } +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + private void loadBrowserSearchPage(String searchName) { + loadFacebookBrowserPage(FACEBOOK_SEARCH_PAGE_URL + searchName); + } + private void loadBrowser1SearchPage(String searchName) { + loadTwitterBrowserPage(TWITTER_SEARCH_PAGE_URL + searchName); + } + +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + @Subscribe + private void handleSearchPersonEvent(SearchPersonEvent event) { + + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + + String platformToSearch = event.getPlatform(); + + if (StringUtil.containsWordIgnoreCase(platformToSearch, Facebook.PLATFORM_KEYWORD) + || StringUtil.containsWordIgnoreCase(platformToSearch, Facebook.PLATFORM_ALIAS)) { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + updateBrowserTabs(FUNCTION_REMOVE, TWITTER_TAB_ID); + loadBrowserSearchPage(event.getSearchName()); + loadTwitterBrowserPage(defaultPage.toExternalForm()); + } else if (StringUtil.containsWordIgnoreCase(platformToSearch, Twitter.PLATFORM_KEYWORD) + || StringUtil.containsWordIgnoreCase(platformToSearch, Twitter.PLATFORM_ALIAS)) { + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + updateBrowserTabs(FUNCTION_REMOVE, FACEBOOK_TAB_ID); + loadFacebookBrowserPage(defaultPage.toExternalForm()); + loadBrowser1SearchPage(event.getSearchName()); + } else { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + loadBrowserSearchPage(event.getSearchName()); + loadBrowser1SearchPage(event.getSearchName()); + } + } +} +``` diff --git a/collated/functional/Nethergale-reused.md b/collated/functional/Nethergale-reused.md new file mode 100644 index 000000000000..283fa95bd28f --- /dev/null +++ b/collated/functional/Nethergale-reused.md @@ -0,0 +1,8 @@ +# Nethergale-reused +###### \java\seedu\address\model\smplatform\Twitter.java +``` java + //Reused from https://stackoverflow.com/questions/6024848/regex-to-validate-a-twitter-url with minor modifications + public static final String LINK_VALIDATION_REGEX = "(?:https?:\\/\\/)?(?:www\\.|m\\.)?twitter\\.com\\/" + + "[^/ \\\\](?:(?:\\w)*#!\\/)?(?:pages\\/)?(?:[\\w\\-]*\\/)*([\\w\\-]*)/?"; + +``` diff --git a/collated/functional/Nethergale.md b/collated/functional/Nethergale.md new file mode 100644 index 000000000000..ee696541ace1 --- /dev/null +++ b/collated/functional/Nethergale.md @@ -0,0 +1,871 @@ +# Nethergale +###### \java\seedu\address\logic\commands\AddPlatformCommand.java +``` java +/** + * Adds social media platforms to the specified person in the address book. + */ +public class AddPlatformCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "addplatform"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds social media platforms to the person " + + "identified by the index number used in the last person listing through website links.\n" + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_LINK + "LINK]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_LINK + "www.facebook.com/johndoe"; + + public static final String MESSAGE_ADD_PLATFORM_SUCCESS = "Platform(s) successfully added to %1$s."; + public static final String MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS = "Platform(s) successfully cleared for %1$s."; + public static final String MESSAGE_LINK_COLLECTION_EMPTY = "At least 1 link field should be specified."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + private final Index index; + private final Map linkMap; + private final Map socialMediaPlatformMap; + + private Person personToEdit; + private Person editedPerson; + + /** + * @param index of the person in the filtered person list to edit + * @param linkMap as defined by the social media platform type as key and link as value + */ + public AddPlatformCommand(Index index, Map linkMap) { + requireNonNull(index); + requireNonNull(linkMap); + + this.index = index; + this.linkMap = linkMap; + socialMediaPlatformMap = new HashMap<>(); + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + requireNonNull(personToEdit); + requireNonNull(editedPerson); + + 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); + + if (socialMediaPlatformMap.isEmpty()) { + return new CommandResult(String.format(MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS, editedPerson.getName())); + } + return new CommandResult(String.format(MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + try { + addToSocialMediaPlatformMap(); + } catch (IllegalValueException ive) { + throw new CommandException(ive.getMessage()); + } + + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(index.getZeroBased()); + addCurrentSocialMediaPlatforms(); + editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), socialMediaPlatformMap, personToEdit.getTags()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddPlatformCommand)) { + return false; + } + + // state check + AddPlatformCommand e = (AddPlatformCommand) other; + return index.equals(e.index) + && linkMap.equals(e.linkMap); + } + + /** + * Constructs social media platform objects depending on the link type and adds them to the map. + */ + private void addToSocialMediaPlatformMap() throws IllegalValueException { + for (String type : linkMap.keySet()) { + socialMediaPlatformMap.put(type, + SocialMediaPlatformFactory.getSocialMediaPlatform(type, linkMap.get(type))); + } + } + + /** + * Adds back the social media platforms not found in the edited person into the map only if it is not empty. + */ + private void addCurrentSocialMediaPlatforms() { + if (!socialMediaPlatformMap.isEmpty()) { + for (String key : personToEdit.getSocialMediaPlatformMap().keySet()) { + socialMediaPlatformMap.putIfAbsent(key, personToEdit.getSocialMediaPlatformMap().get(key)); + } + } + } +} +``` +###### \java\seedu\address\logic\commands\RemovePlatformCommand.java +``` java +/** + * Removes the specified social media platforms of a person identified + * using it's last displayed index from the address book. + */ +public class RemovePlatformCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "removeplatform"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes specified social media platforms " + + "of the person identified by the index number used in the last person listing.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_SOCIAL_MEDIA_PLATFORM + "PLATFORM]...\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_SOCIAL_MEDIA_PLATFORM + "facebook"; + + public static final String MESSAGE_REMOVE_PLATFORM_SUCCESS = "Platform(s) removed from %1$s."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_PLATFORM_MAP_NOT_EDITED = "No changes can be made from the given platform info."; + + private final Index targetIndex; + private final Set platformsToRemove; + private final Map socialMediaPlatformMap; + + private Person personToEdit; + private Person editedPerson; + + public RemovePlatformCommand(Index targetIndex, Set platformsToRemove) { + this.targetIndex = targetIndex; + this.platformsToRemove = platformsToRemove; + socialMediaPlatformMap = new HashMap<>(); + } + + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + 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"); + } + + return new CommandResult(String.format(MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(targetIndex.getZeroBased()); + + try { + removeFromSocialMediaPlatformMap(); + } catch (IllegalValueException ive) { + throw new CommandException(ive.getMessage()); + } + + editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), socialMediaPlatformMap, personToEdit.getTags()); + } + + /** + * Removes the social media platform mappings from the map if specified. + * + * @throws IllegalValueException if no changes are made + */ + private void removeFromSocialMediaPlatformMap() throws IllegalValueException { + if (!platformsToRemove.isEmpty()) { + socialMediaPlatformMap.putAll(personToEdit.getSocialMediaPlatformMap()); + + for (String platform : platformsToRemove) { + socialMediaPlatformMap.remove(platform.toLowerCase()); + } + + if (socialMediaPlatformMap.equals(personToEdit.getSocialMediaPlatformMap())) { + throw new IllegalValueException(MESSAGE_PLATFORM_MAP_NOT_EDITED); + } + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RemovePlatformCommand // instanceof handles nulls + && this.targetIndex.equals(((RemovePlatformCommand) other).targetIndex) // state check + && this.platformsToRemove.equals(((RemovePlatformCommand) other).platformsToRemove)); + } +} +``` +###### \java\seedu\address\logic\commands\SortCommand.java +``` java +/** + * Displays the current list of persons in the address book to the user sorted alphabetically. + */ +public class SortCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays the current list of persons in the " + + "address book sorted in alphabetical order with index numbers.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Address book sorted successfully!"; + public static final String MESSAGE_FAILURE = "Address book has already been sorted."; + + private List personList; + + /** + * Returns true if person list is sorted. + */ + private boolean isListSorted() { + return Ordering.from(Person.nameComparator()).isOrdered(personList); + } + + @Override + public CommandResult executeUndoableCommand() { + requireNonNull(model); + model.sortAllPersons(); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + personList = model.getAddressBook().getPersonList(); + if (personList.isEmpty()) { + throw new CommandException(Messages.MESSAGE_ADDRESS_BOOK_EMPTY); + } + if (isListSorted()) { + throw new CommandException(MESSAGE_FAILURE); + } + } +} +``` +###### \java\seedu\address\logic\parser\AddPlatformCommandParser.java +``` java +/** + * Parses input arguments and creates a new AddPlatformCommand object + */ +public class AddPlatformCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddPlatformCommand + * and returns an AddPlatformCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public AddPlatformCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_LINK); + + Index index; + Map linkMap; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPlatformCommand.MESSAGE_USAGE)); + } + + try { + linkMap = parseLinksForAddPlatform(argMultimap.getAllValues(PREFIX_LINK)); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + + return new AddPlatformCommand(index, linkMap); + } + + /** + * Parses {@code Collection links} into a {@code Map} if {@code links} is non-empty. + * If {@code links} contain only one element which is an empty string, it will be parsed into a + * {@code Map} containing zero links. + * + * @throws IllegalValueException if user does not specify even a single link prefix. + */ + private Map parseLinksForAddPlatform(Collection links) throws IllegalValueException { + assert links != null; + + if (links.isEmpty()) { + throw new IllegalValueException(AddPlatformCommand.MESSAGE_LINK_COLLECTION_EMPTY); + } + Collection linkSet = links.size() == 1 && links.contains("") ? Collections.emptySet() : links; + return ParserUtil.parseLinks(linkSet); + } +} +``` +###### \java\seedu\address\logic\parser\AddressBookParser.java +``` java + case SortCommand.COMMAND_WORD: + return new SortCommand(); + + case AddPlatformCommand.COMMAND_WORD: + return new AddPlatformCommandParser().parse(arguments); + + case RemovePlatformCommand.COMMAND_WORD: + return new RemovePlatformCommandParser().parse(arguments); + +``` +###### \java\seedu\address\logic\parser\ParserUtil.java +``` java + /** + * Parses a {@code String link} into a {@code Link}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code link} is invalid. + */ + public static Link parseLink(String link) throws IllegalValueException { + requireNonNull(link); + String trimmedLink = link.trim(); + if (!Link.isValidLink(trimmedLink)) { + throw new IllegalValueException(Link.MESSAGE_INVALID_LINK); + } + return new Link(trimmedLink); + } + + /** + * Parses {@code Collection links} into a {@code Map}. + * + * @throws IllegalValueException if any social media platform is going to have more than one link each. + */ + public static Map parseLinks(Collection links) throws IllegalValueException { + requireNonNull(links); + final Map linkMap = new HashMap<>(); + for (String linkStr : links) { + String linkType = Link.getLinkType(linkStr); + Link link = parseLink(linkStr); + if ((linkType.equals(Link.FACEBOOK_LINK_TYPE) || linkType.equals(Link.TWITTER_LINK_TYPE)) + && linkMap.containsKey(linkType)) { + throw new IllegalValueException(Link.MESSAGE_LINK_CONSTRAINTS); + } + linkMap.put(linkType, link); + } + return linkMap; + } + +``` +###### \java\seedu\address\logic\parser\RemovePlatformCommandParser.java +``` java +/** + * Parses input arguments and creates a new RemovePlatformCommand object + */ +public class RemovePlatformCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemovePlatformCommand + * and returns a RemovePlatformCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemovePlatformCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SOCIAL_MEDIA_PLATFORM); + + Index index; + Set platformSet = new HashSet<>(); + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + platformSet.addAll(argMultimap.getAllValues(PREFIX_SOCIAL_MEDIA_PLATFORM)); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemovePlatformCommand.MESSAGE_USAGE)); + } + + return new RemovePlatformCommand(index, platformSet); + } + +} +``` +###### \java\seedu\address\model\AddressBook.java +``` java + /** + * Sorts all persons by name in alphabetical order in the address book. + */ + public void sort() { + persons.sort(); + } + +``` +###### \java\seedu\address\model\ModelManager.java +``` java + @Override + public void sortAllPersons() { + addressBook.sort(); + indicateAddressBookChanged(); + } + +``` +###### \java\seedu\address\model\person\Person.java +``` java + /** + * Returns an immutable social media platform map, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Map getSocialMediaPlatformMap() { + return Collections.unmodifiableMap(smpMap); + } + +``` +###### \java\seedu\address\model\person\Person.java +``` java + /** + * Returns a person comparator, which compares the names alphabetically. + * Similar names are compared lexicographically. + */ + public static Comparator nameComparator() { + return Comparator.comparing((Person p) -> p.getName().toString(), ( + s1, s2) -> (s1.compareToIgnoreCase(s2) == 0) ? s1.compareTo(s2) : s1.compareToIgnoreCase(s2)); + } + +``` +###### \java\seedu\address\model\person\UniquePersonList.java +``` java + /** + * Sorts all persons in list alphabetically. Similar names are sorted lexicographically. + */ + public void sort() { + internalList.sort(Person.nameComparator()); + } + +``` +###### \java\seedu\address\model\smplatform\Facebook.java +``` java +/** + * Represents a facebook object. + */ +public class Facebook extends SocialMediaPlatform { + + public static final String PLATFORM_KEYWORD = "facebook"; + + public static final String PLATFORM_ALIAS = "fb"; + + //Code adapted from https://stackoverflow.com/questions/5205652/facebook-profile-url-regular-expression + public static final String LINK_VALIDATION_REGEX = "(?:https?:\\/\\/)?(?:www\\.|m\\.)?facebook\\.com\\/" + + "(?:profile.php\\?id=(?=\\d.*))?" + + "[^/ \\\\](?:(?:\\w)*#!\\/)?(?:pages\\/)?(?:[\\w\\-]*\\/)*([\\w\\-\\.]*)/?"; + + public Facebook(Link link) { + this.link = link; + } +} +``` +###### \java\seedu\address\model\smplatform\Link.java +``` java +/** + * Represents a SocialMediaPlatform's link. + * Guarantees: immutable; is always valid + */ +public class Link { + public static final String MESSAGE_LINK_CONSTRAINTS = "Only one link is allowed for each social media platform."; + public static final String MESSAGE_INVALID_LINK = "Links should be valid Facebook or Twitter profile links."; + public static final String FACEBOOK_LINK_TYPE = "facebook"; + public static final String TWITTER_LINK_TYPE = "twitter"; + public static final String UNKNOWN_LINK_TYPE = "unknown"; + + private static final String FACEBOOK_LINK_SIGNATURE = "facebook.com"; + private static final String TWITTER_LINK_SIGNATURE = "twitter.com"; + private static final String LONGEST_VALID_LINK_PREFIX = "https://www."; + + public final String value; + + public Link(String link) { + requireNonNull(link); + this.value = link; + } + + /** + * Returns the social media platform type of the link. + */ + public static String getLinkType(String link) { + if (link.contains(TWITTER_LINK_SIGNATURE) + && !(link.indexOf(TWITTER_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return TWITTER_LINK_TYPE; + } else if (link.contains(FACEBOOK_LINK_SIGNATURE) + && !(link.indexOf(FACEBOOK_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return FACEBOOK_LINK_TYPE; + } else { + return UNKNOWN_LINK_TYPE; + } + } + + /** + * Returns true if a given string is a valid link. + */ + public static boolean isValidLink(String test) { + if (test.contains(TWITTER_LINK_SIGNATURE) + && !(test.indexOf(TWITTER_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return test.matches(Twitter.LINK_VALIDATION_REGEX); + } else if (test.contains(FACEBOOK_LINK_SIGNATURE) + && !(test.indexOf(FACEBOOK_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return test.matches(Facebook.LINK_VALIDATION_REGEX); + } else { + return false; + } + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Link // instanceof handles nulls + && this.value.equals(((Link) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` +###### \java\seedu\address\model\smplatform\SocialMediaPlatform.java +``` java + protected Link link; + + public Link getLink() { + return link; + } + +``` +###### \java\seedu\address\model\smplatform\SocialMediaPlatformFactory.java +``` java +/** + * Acts as a social media platform creator. + * Determines the different types of social media platform objects to be created by using the link and its type. + */ +public final class SocialMediaPlatformFactory { + public static final String MESSAGE_BUILD_ERROR = "Social media platform cannot be constructed. " + + "Link type is unrecognised or mismatched with link."; + + /** + * Don't let anyone instantiate this class. + */ + private SocialMediaPlatformFactory() {} + + /** + * Constructs the specific social media platform object by using the {@code type} and setting the {@code link} + * as a parameter. + * + * @return the created social media platform object + * @throws IllegalValueException if type is not recognised + */ + public static SocialMediaPlatform getSocialMediaPlatform(String type, Link link) throws IllegalValueException { + if (type.equals(Link.FACEBOOK_LINK_TYPE) && type.equals(Link.getLinkType(link.value))) { + return new Facebook(link); + } else if (type.equals(Link.TWITTER_LINK_TYPE) && type.equals(Link.getLinkType(link.value))) { + return new Twitter(link); + } else { + throw new IllegalValueException(MESSAGE_BUILD_ERROR); + } + } +} +``` +###### \java\seedu\address\model\smplatform\Twitter.java +``` java + public Twitter(Link link) { + this.link = link; + } +} +``` +###### \java\seedu\address\model\util\SampleDataUtil.java +``` java + /** + * Returns a social media platform map mapping to the various platforms, + * using the list of strings given as the links. Unknown link types and invalid links will be ignored. + * Only the first link will be added if there are multiple links of the same type. + */ + public static Map getSocialMediaPlatformMap(String... strings) { + HashMap smpMap = new HashMap<>(); + for (String s : strings) { + String type = Link.getLinkType(s); + if (type.equals(Link.FACEBOOK_LINK_TYPE) && Link.isValidLink(s)) { + smpMap.putIfAbsent(type, new Facebook(new Link(s))); + } else if (type.equals(Link.TWITTER_LINK_TYPE) && Link.isValidLink(s)) { + smpMap.putIfAbsent(type, new Twitter(new Link(s))); + } + } + + return smpMap; + } + +``` +###### \java\seedu\address\storage\XmlAdaptedPerson.java +``` java + platforms = new ArrayList<>(); + for (String key : source.getSocialMediaPlatformMap().keySet()) { + platforms.add( + new XmlAdaptedSocialMediaPlatform(key, source.getSocialMediaPlatformMap().get(key).getLink())); + } + +``` +###### \java\seedu\address\storage\XmlAdaptedPerson.java +``` java + final Map personSocialMediaPlatforms = new HashMap<>(); + for (XmlAdaptedSocialMediaPlatform platform : platforms) { + SocialMediaPlatform platformModel = platform.toModelType(); + personSocialMediaPlatforms.put(Link.getLinkType(platformModel.getLink().value), platformModel); + } + +``` +###### \java\seedu\address\storage\XmlAdaptedSocialMediaPlatform.java +``` java +/** + * JAXB-friendly adapted version of the SocialMediaPlatform. + */ +public class XmlAdaptedSocialMediaPlatform { + + @XmlElement + private String type; + @XmlElement + private String link; + + /** + * Constructs an XmlAdaptedSocialMediaPlatform. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedSocialMediaPlatform() {} + + /** + * Constructs a {@code XmlAdaptedSocialMediaPlatform} with the given {@code type} and {@code link}. + */ + public XmlAdaptedSocialMediaPlatform(String type, String link) { + this.type = type; + this.link = link; + } + + /** + * Converts a given String and Link into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedSocialMediaPlatform + */ + public XmlAdaptedSocialMediaPlatform(String type, Link source) { + this.type = type; + link = source.value; + } + + /** + * Converts this jaxb-friendly adapted social media platform object into the model's social media platform object. + * + * @throws IllegalValueException if link is not valid + */ + public SocialMediaPlatform toModelType() throws IllegalValueException { + if (!Link.isValidLink(link)) { + throw new IllegalValueException(Link.MESSAGE_INVALID_LINK); + } + + return SocialMediaPlatformFactory.getSocialMediaPlatform(type, new Link(link)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedSocialMediaPlatform)) { + return false; + } + + return type.equals(((XmlAdaptedSocialMediaPlatform) other).type) + && link.equals(((XmlAdaptedSocialMediaPlatform) other).link); + } +} +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + openTabIdSet = tabPane.getTabs().stream().map(tab -> tab.getId()).collect(Collectors.toSet()); + +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + /** + * Updates the display of social media browser tabs. + * + * @param function defines an add or remove function + * @param tabId id of the fxml tab + */ + private void updateBrowserTabs(String function, String tabId) { + switch (function) { + + case FUNCTION_ADD: + addBrowserTab(tabId); + break; + + case FUNCTION_REMOVE: + removeBrowserTab(tabId); + break; + + default: + // Do nothing + } + } + + /** + * Adds the specified browser tab to the UI using the {@code tabId} if it is not open; + */ + private void addBrowserTab(String tabId) { + if (!openTabIdSet.contains(tabId)) { + openTabIdSet.add(tabId); + switch (tabId) { + + case FACEBOOK_TAB_ID: + tabPane.getTabs().add(0, facebookTab); + break; + + case TWITTER_TAB_ID: + tabPane.getTabs().add(twitterTab); + break; + + default: + // Do nothing + } + } + } + + /** + * Removes the specified browser tab from the UI using the {@code tabId}; + */ + private void removeBrowserTab(String tabId) { + openTabIdSet.remove(tabId); + switch (tabId) { + + case FACEBOOK_TAB_ID: + tabPane.getTabs().remove(facebookTab); + break; + + case TWITTER_TAB_ID: + tabPane.getTabs().remove(twitterTab); + break; + + default: + //Do nothing + } + } + + /** + * Returns the given {@code url} with a protocol and subdomain if unspecified. + */ + public static String parseUrl(String url) { + if (!url.contains("://")) { + if (!url.contains("www")) { + return "https://www." + url; + } + return "https://" + url; + } else { + if (!url.contains("www")) { + String[] splitUrl = url.split("://"); + return "https://www." + splitUrl[1]; + } + } + + return url; + } + + /** + * Loads the Facebook profile page for the browser on the Facebook tab if it exists. + */ + private void loadFacebookBrowserProfilePage(Person person) { + if (person.getSocialMediaPlatformMap().containsKey(Link.FACEBOOK_LINK_TYPE)) { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + String url = person.getSocialMediaPlatformMap().get(Link.FACEBOOK_LINK_TYPE).getLink().value; + loadFacebookBrowserPage(parseUrl(url)); + } else { + updateBrowserTabs(FUNCTION_REMOVE, FACEBOOK_TAB_ID); + loadFacebookBrowserPage(defaultPage.toExternalForm()); + } + } + + /** + * Loads the Twitter profile page for the browser on the Twitter tab if it exists. + */ + private void loadTwitterBrowserProfilePage(Person person) { + if (person.getSocialMediaPlatformMap().containsKey(Link.TWITTER_LINK_TYPE)) { + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + String url = person.getSocialMediaPlatformMap().get(Link.TWITTER_LINK_TYPE).getLink().value; + loadTwitterBrowserPage(parseUrl(url)); + } else { + updateBrowserTabs(FUNCTION_REMOVE, TWITTER_TAB_ID); + loadTwitterBrowserPage(defaultPage.toExternalForm()); + } + } + +``` +###### \java\seedu\address\ui\PersonCard.java +``` java + /** + * Adds the various social media icon tab to the social media icon pane. + */ + private void addIconsToSocialMediaIconPane() { + for (String key : person.getSocialMediaPlatformMap().keySet()) { + if (!person.getSocialMediaPlatformMap().get(key).getLink().value.isEmpty()) { + socialMediaIconPane.getChildren().add(createSocialMediaIconTab(key)); + } + } + } + + /** + * Creates the icon tab for the specific social media platform. + * + * @param type social media platform type + * @return stack pane representing an icon tab + */ + private StackPane createSocialMediaIconTab(String type) { + StackPane socialMediaIconTab = new StackPane(); + Region tabBackground = new Region(); + tabBackground.setPrefSize(30, 32); + tabBackground.setStyle("-fx-background-color: linear-gradient(from 0% 0% to 100% 0%, #29323C, #485563); " + + "-fx-background-radius: 6 0 0 6; -fx-border-color: #242C35; " + + "-fx-border-radius: 6 0 0 6; -fx-border-width: 2 0 2 2;"); + ImageView tabIcon = new ImageView(); + String url = imageUrl(type); + if (!url.isEmpty()) { + tabIcon.setFitWidth(20); + tabIcon.setFitHeight(20); + tabIcon.setImage(new Image(url)); + socialMediaIconTab.getChildren().addAll(tabBackground, tabIcon); + } + return socialMediaIconTab; + } + + /** + * Returns the location of the icon for the specific social media platform {@code type}. + */ + private String imageUrl(String type) { + if (type.equals(Link.FACEBOOK_LINK_TYPE)) { + return "images/facebook_icon.png"; + } else if (type.equals(Link.TWITTER_LINK_TYPE)) { + return "images/twitter_icon.png"; + } else { + return ""; + } + } +} +``` +###### \resources\view\PersonListCard.fxml +``` fxml + + + +``` diff --git a/collated/functional/shadow2496.md b/collated/functional/shadow2496.md new file mode 100644 index 000000000000..a6bead774a5d --- /dev/null +++ b/collated/functional/shadow2496.md @@ -0,0 +1,491 @@ +# shadow2496 +###### \java\seedu\address\commons\events\ui\NewResultAvailableEvent.java +``` java + public final boolean hasError; + +``` +###### \java\seedu\address\commons\events\ui\NewResultAvailableEvent.java +``` java + this.hasError = hasError; + +``` +###### \java\seedu\address\commons\events\ui\ShowLoginDialogRequestEvent.java +``` java +/** + * Indicates a request to view the login dialog. + */ +public class ShowLoginDialogRequestEvent extends BaseEvent { + + public final String loadUrl; + + public ShowLoginDialogRequestEvent(String loadUrl) { + this.loadUrl = loadUrl; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` +###### \java\seedu\address\logic\commands\LoginCommand.java +``` java +/** + * Logs into a social media platform using the user's account information. + */ +public class LoginCommand extends Command { + + public static final String COMMAND_WORD = "login"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Logs into a social media platform using " + + "the user's name and password. " + + "Parameters: " + + PREFIX_USERNAME + "USERNAME " + + PREFIX_PASSWORD + "PASSWORD\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_USERNAME + "johndoe " + + PREFIX_PASSWORD + "jd9876"; + + public static final String MESSAGE_SUCCESS = "Logged in account: %1$s"; + + private final Account accountToLogin; + + public LoginCommand(Account account) { + requireNonNull(account); + accountToLogin = account; + } + + @Override + public CommandResult execute() { + requireNonNull(model); + model.loginAccount(accountToLogin); + return new CommandResult(String.format(MESSAGE_SUCCESS, accountToLogin)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof LoginCommand // instanceof handles nulls + && this.accountToLogin.equals(((LoginCommand) other).accountToLogin)); // state check + } +} +``` +###### \java\seedu\address\logic\Logic.java +``` java + /** Passes the verification code for an access token */ + void passVerificationCode(String code); + +``` +###### \java\seedu\address\logic\LogicManager.java +``` java + @Override + public void passVerificationCode(String code) { + model.setVerificationCode(code); + } + +``` +###### \java\seedu\address\logic\parser\AddressBookParser.java +``` java + case LoginCommand.COMMAND_WORD: + return new LoginCommandParser().parse(arguments); + +``` +###### \java\seedu\address\logic\parser\LoginCommandParser.java +``` java +/** + * Parses input arguments and creates a new LoginCommand object + */ +public class LoginCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoginCommand + * and returns an LoginCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LoginCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_USERNAME, PREFIX_PASSWORD); + + if (!arePrefixesPresent(argMultimap, PREFIX_USERNAME, PREFIX_PASSWORD) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + + try { + Username username = ParserUtil.parseUsername(argMultimap.getValue(PREFIX_USERNAME)).get(); + Password password = ParserUtil.parsePassword(argMultimap.getValue(PREFIX_PASSWORD)).get(); + + Account account = new Account(username, password); + + return new LoginCommand(account); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} +``` +###### \java\seedu\address\logic\parser\ParserUtil.java +``` java + /** + * Parses a {@code String username} into an {@code Username}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code username} is invalid. + */ + public static Username parseUsername(String username) throws IllegalValueException { + requireNonNull(username); + String trimmedUsername = username.trim(); + if (!Username.isValidUsername(trimmedUsername)) { + throw new IllegalValueException(Username.MESSAGE_USERNAME_CONSTRAINTS); + } + return new Username(trimmedUsername); + } + + /** + * Parses a {@code Optional username} into an {@code Optional} if {@code username} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseUsername(Optional username) throws IllegalValueException { + requireNonNull(username); + return username.isPresent() ? Optional.of(parseUsername(username.get())) : Optional.empty(); + } + + /** + * Parses a {@code String password} into an {@code Password}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code password} is invalid. + */ + public static Password parsePassword(String password) throws IllegalValueException { + requireNonNull(password); + String trimmedPassword = password.trim(); + if (!Password.isValidPassword(trimmedPassword)) { + throw new IllegalValueException(Password.MESSAGE_PASSWORD_CONSTRAINTS); + } + return new Password(trimmedPassword); + } + + /** + * Parses a {@code Optional password} into an {@code Optional} if {@code password} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parsePassword(Optional password) throws IllegalValueException { + requireNonNull(password); + return password.isPresent() ? Optional.of(parsePassword(password.get())) : Optional.empty(); + } + +``` +###### \java\seedu\address\model\account\Account.java +``` java +/** + * Represents an Account in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Account { + + private static final String APP_ID = "366902457122089"; + private static final String APP_SECRET = "9cdbed37c0780e75e995381f0688a8d7"; + private static final String REDIRECT_URL = "https://www.facebook.com/connect/login_success.html"; + + private final Username username; + private final Password password; + + private ScopeBuilder scopeBuilder; + private FacebookClient client; + private String code; + + /** + * Every field must be present and not null. + */ + public Account(Username username, Password password) { + requireAllNonNull(username, password); + this.username = username; + this.password = password; + + setScopeBuilder(); + setClient(); + } + + public Username getUsername() { + return username; + } + + public Password getPassword() { + return password; + } + + /** + * Sets the scope builder which is a set of permissions. + */ + private void setScopeBuilder() { + scopeBuilder = new ScopeBuilder(); + } + + /** + * Sets the client used to get an access token. + */ + private void setClient() { + client = new DefaultFacebookClient(Version.VERSION_2_12); + } + + public void setCode(String code) { + this.code = code; + } + + /** + * Returns the login dialog url using {@link #client}. + */ + public String getLoginDialogUrl() { + return client.getLoginDialogUrl(APP_ID, REDIRECT_URL, scopeBuilder); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Account)) { + return false; + } + + Account otherAccount = (Account) other; + return otherAccount.getUsername().equals(this.getUsername()) + && otherAccount.getPassword().equals(this.getPassword()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(username, password); + } + + @Override + public String toString() { + return "Username: " + getUsername() + " Password: " + getPassword(); + } +} +``` +###### \java\seedu\address\model\account\Password.java +``` java +/** + * Represents an Account's password in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPassword(String)} + */ +public class Password { + + public static final String MESSAGE_PASSWORD_CONSTRAINTS = + "Account passwords can take any values, and it should not be blank"; + + /* + * The first character of the password must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String PASSWORD_VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs an {@code Password}. + * + * @param password A valid password. + */ + public Password(String password) { + requireNonNull(password); + checkArgument(isValidPassword(password), MESSAGE_PASSWORD_CONSTRAINTS); + this.value = password; + } + + /** + * Returns true if a given string is a valid account password. + */ + public static boolean isValidPassword(String test) { + return test.matches(PASSWORD_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Password // instanceof handles nulls + && this.value.equals(((Password) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` +###### \java\seedu\address\model\account\Username.java +``` java +/** + * Represents an Account's username in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidUsername(String)} + */ +public class Username { + + public static final String MESSAGE_USERNAME_CONSTRAINTS = + "Account usernames can take any values, and it should not be blank"; + + /* + * The first character of the username must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String USERNAME_VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs an {@code Username}. + * + * @param username A valid username. + */ + public Username(String username) { + requireNonNull(username); + checkArgument(isValidUsername(username), MESSAGE_USERNAME_CONSTRAINTS); + this.value = username; + } + + /** + * Returns true if a given string is a valid account username. + */ + public static boolean isValidUsername(String test) { + return test.matches(USERNAME_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Username // instanceof handles nulls + && this.value.equals(((Username) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} +``` +###### \java\seedu\address\model\AddressBook.java +``` java + //// account-level operations + + /** + * Logs into a social media platform using {@code account}. + */ + public void loginAccount(Account account) { + this.account = account; + String loadUrl = this.account.getLoginDialogUrl(); + EventsCenter.getInstance().post(new ShowLoginDialogRequestEvent(loadUrl)); + } + + public void setVerificationCode(String code) { + account.setCode(code); + } + +``` +###### \java\seedu\address\model\Model.java +``` java + /** Logs in with the given account */ + void loginAccount(Account account); + + /** Sets the verification code in an account. */ + void setVerificationCode(String code); + +``` +###### \java\seedu\address\model\ModelManager.java +``` java + @Override + public void loginAccount(Account account) { + addressBook.loginAccount(account); + //indicateAddressBookChanged(); + } + + @Override + public void setVerificationCode(String code) { + addressBook.setVerificationCode(code); + } + +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + /** + * Passes a verification code when the login is successful. + */ + private void passVerificationCode() { + facebookBrowser.getEngine().getLoadWorker().stateProperty().addListener(( + ObservableValue observable, Worker.State oldValue, Worker.State newValue) -> { + if (newValue != Worker.State.SUCCEEDED) { + return; + } + + String currentUrl = facebookBrowser.getEngine().getLocation(); + + if (currentUrl.endsWith(DEFAULT_PAGE)) { + } else if (currentUrl.startsWith(SUCCESS_URL)) { + int pos = currentUrl.indexOf("code="); + logic.passVerificationCode(currentUrl.substring(pos + "code=".length())); + } + }); + } + +``` +###### \java\seedu\address\ui\BrowserPanel.java +``` java + @Subscribe + private void handleShowLoginDialogRequestEvent(ShowLoginDialogRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + loadFacebookBrowserPage(event.loadUrl); + passVerificationCode(); + } +} +``` +###### \java\seedu\address\ui\ResultDisplay.java +``` java + if (event.hasError) { + setStyleToIndicateResultError(); + } else { + setStyleToDefault(); + } + +``` +###### \java\seedu\address\ui\ResultDisplay.java +``` java + /** + * Sets the result display style to use the default style. + */ + private void setStyleToDefault() { + resultDisplay.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the result display style to indicate that a result has an error. + */ + private void setStyleToIndicateResultError() { + ObservableList styleClass = resultDisplay.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } +} +``` diff --git a/collated/test/KevinChuangCH.md b/collated/test/KevinChuangCH.md new file mode 100644 index 000000000000..379b8c6f01fe --- /dev/null +++ b/collated/test/KevinChuangCH.md @@ -0,0 +1,277 @@ +# KevinChuangCH +###### \java\seedu\address\logic\commands\FindWithTagCommandTest.java +``` java +/** + * Contains integration tests (interaction with the Model) for {@code FindWithTagCommand}. + */ +public class FindWithTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + TagContainsKeywordsPredicate firstPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("first")); + TagContainsKeywordsPredicate secondPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("second")); + + FindWithTagCommand findFirstCommand = new FindWithTagCommand(firstPredicate); + FindWithTagCommand findSecondCommand = new FindWithTagCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindWithTagCommand findFirstCommandCopy = new FindWithTagCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + FindWithTagCommand command = prepareCommand(" "); + assertCommandSuccess(command, expectedMessage, Collections.emptyList()); + } + + @Test + public void execute_multipleKeywords_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + FindWithTagCommand command = prepareCommand("classmate PC3196 labPartner"); + assertCommandSuccess(command, expectedMessage, Arrays.asList(CARL, ELLE, FIONA)); + } + + /** + * Parses {@code userInput} into a {@code FindWithTagCommand}. + */ + private FindWithTagCommand prepareCommand(String userInput) { + FindWithTagCommand command = + new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+")))); + command.setData(model, new CommandHistory(), new UndoRedoStack()); + return command; + } + + /** + * Asserts that {@code command} is successfully executed, and
+ * - the command feedback is equal to {@code expectedMessage}
+ * - the {@code FilteredList} is equal to {@code expectedList}
+ * - the {@code AddressBook} in model remains the same after executing the {@code command} + */ + private void assertCommandSuccess(FindWithTagCommand command, String expectedMessage, List expectedList) { + AddressBook expectedAddressBook = new AddressBook(model.getAddressBook()); + CommandResult commandResult = command.execute(); + + assertEquals(expectedMessage, commandResult.feedbackToUser); + assertEquals(expectedList, model.getFilteredPersonList()); + assertEquals(expectedAddressBook, model.getAddressBook()); + } +} +``` +###### \java\seedu\address\logic\parser\FindWithTagCommandParserTest.java +``` java +public class FindWithTagCommandParserTest { + + private FindWithTagCommandParser parser = new FindWithTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindWithTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindWithTagCommand() { + // no leading and trailing whitespaces + FindWithTagCommand expectedFindWithTagCommand = + new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList("neighbour", "owesMoney"))); + assertParseSuccess(parser, "neighbour owesMoney", expectedFindWithTagCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n neighbour \n \t owesMoney \t", expectedFindWithTagCommand); + } + +} +``` +###### \java\systemtests\FindWithTagCommandSystemTest.java +``` java +public class FindWithTagCommandSystemTest extends AddressBookSystemTest { + + @Test + public void find() { + /* Case: find multiple persons in address book, command with leading spaces and trailing spaces + * -> 2 persons found + */ + String command = " " + FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY + " "; + Model expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, BENSON, DANIEL); // Benson and Daniel have the tag "armyBuddy" + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: repeat previous find command where person list is displaying the persons we are finding + * -> 2 persons found + */ + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person where person list is not displaying the person we are finding -> 1 person found */ + command = FindWithTagCommand.COMMAND_WORD + " classmate"; + ModelHelper.setFilteredList(expectedModel, CARL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " owesMoney roommate"; + ModelHelper.setFilteredList(expectedModel, BENSON, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords in reversed order -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords with 1 repeat -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney roommate"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 matching keywords and 1 non-matching keyword + * -> 2 persons found + */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney NonMatchingKeyWord"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: undo previous find command -> rejected */ + command = UndoCommand.COMMAND_WORD; + String expectedResultMessage = UndoCommand.MESSAGE_FAILURE; + assertCommandFailure(command, expectedResultMessage); + + /* Case: redo previous find command -> rejected */ + command = RedoCommand.COMMAND_WORD; + expectedResultMessage = RedoCommand.MESSAGE_FAILURE; + assertCommandFailure(command, expectedResultMessage); + + /* Case: find same persons in address book after deleting 1 of them -> 1 person found */ + executeCommand(DeleteCommand.COMMAND_WORD + " 1"); + assertFalse(getModel().getAddressBook().getPersonList().contains(BENSON)); + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, keyword is same as tag but of different case -> 1 person found */ + command = FindWithTagCommand.COMMAND_WORD + " ArMyBuDdY"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, keyword is substring of tag -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " armyBud"; + ModelHelper.setFilteredList(expectedModel); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, name is substring of keyword -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " armyBuddies"; + ModelHelper.setFilteredList(expectedModel); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person not in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " girlfriend"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find name of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getName().fullName; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find phone number of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getPhone().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find address of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getAddress().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find email of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getEmail().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find while a person is selected -> selected card deselected */ + showAllPersons(); + selectPerson(Index.fromOneBased(1)); + assertFalse(getPersonListPanel().getHandleToSelectedCard().getName().equals(DANIEL.getName().fullName)); + command = FindWithTagCommand.COMMAND_WORD + " roommate"; + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardDeselected(); + + /* Case: find person in empty address book -> 0 persons found */ + deleteAllPersons(); + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: mixed case command word -> rejected */ + command = "FiNdTaG armyBuddy"; + assertCommandFailure(command, MESSAGE_UNKNOWN_COMMAND); + } + + /** + * Executes {@code command} and verifies that the command box displays an empty string, the result display + * box displays {@code Messages#MESSAGE_PERSONS_LISTED_OVERVIEW} with the number of people in the filtered list, + * and the model related components equal to {@code expectedModel}. + * These verifications are done by + * {@code AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model)}.
+ * Also verifies that the status bar remains unchanged, and the command box has the default style class, and the + * selected card updated accordingly, depending on {@code cardStatus}. + * @see AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model) + */ + private void assertCommandSuccess(String command, Model expectedModel) { + String expectedResultMessage = String.format( + MESSAGE_PERSONS_LISTED_OVERVIEW, expectedModel.getFilteredPersonList().size()); + + executeCommand(command); + assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); + assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); + assertStatusBarUnchanged(); + } + + /** + * Executes {@code command} and verifies that the command box displays {@code command}, the result display + * box displays {@code expectedResultMessage} and the model related components equal to the current model. + * These verifications are done by + * {@code AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model)}.
+ * Also verifies that the browser url, selected card and status bar remain unchanged, and the command box has the + * error style. + * @see AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model) + */ + private void assertCommandFailure(String command, String expectedResultMessage) { + Model expectedModel = getModel(); + + executeCommand(command); + assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); + assertSelectedCardUnchanged(); + assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); + assertStatusBarUnchanged(); + } +} +``` diff --git a/collated/test/Nethergale-reused.md b/collated/test/Nethergale-reused.md new file mode 100644 index 000000000000..b4c8a8a512be --- /dev/null +++ b/collated/test/Nethergale-reused.md @@ -0,0 +1,17 @@ +# Nethergale-reused +###### \java\seedu\address\logic\commands\SortCommandTest.java +``` java + //Reused from https://github.com/nus-cs2103-AY1718S2/addressbook-level3/ + //blob/master/test/java/seedu/addressbook/logic/LogicTest.java with minor modifications + /** + * Creates a list of Persons based on the given Person objects. + */ + private List generatePersonList(Person... persons) { + List personList = new ArrayList<>(); + for (Person p : persons) { + personList.add(p); + } + return personList; + } + +``` diff --git a/collated/test/Nethergale.md b/collated/test/Nethergale.md new file mode 100644 index 000000000000..ec0dd10801bc --- /dev/null +++ b/collated/test/Nethergale.md @@ -0,0 +1,1032 @@ +# Nethergale +###### \java\guitests\guihandles\BrowserPanelHandle.java +``` java + public static final String FACEBOOK_BROWSER_ID = "#facebookBrowser"; + public static final String TWITTER_BROWSER_ID = "#twitterBrowser"; + public static final String TAB_PANE_ID = "#tabPane"; + + private boolean isWebViewLoaded = true; + + private URL lastRememberedUrl; + + private WebView facebookWebView; + private WebView twitterWebView; + + public BrowserPanelHandle(Node browserPanelNode) { + super(browserPanelNode); + + facebookWebView = getChildNode(FACEBOOK_BROWSER_ID); // browser for facebookTab + WebEngine facebookEngine = facebookWebView.getEngine(); + new GuiRobot().interact(() -> facebookEngine.getLoadWorker().stateProperty().addListener(( + obs, oldState, newState) -> { + if (newState == Worker.State.RUNNING) { + isWebViewLoaded = false; + } else if (newState == Worker.State.SUCCEEDED) { + isWebViewLoaded = true; + } + })); + + twitterWebView = getChildNode(TWITTER_BROWSER_ID); // browser for twitterTab + WebEngine twitterEngine = twitterWebView.getEngine(); + new GuiRobot().interact(() -> twitterEngine.getLoadWorker().stateProperty().addListener(( + obs, oldState, newState) -> { + if (newState == Worker.State.RUNNING) { + isWebViewLoaded = false; + } else if (newState == Worker.State.SUCCEEDED) { + isWebViewLoaded = true; + } + })); + } + + /** + * Returns the {@code URL} of the currently loaded page for the default browser tab (i.e. facebookTab). + */ + public URL getLoadedUrl() { + URL loadedUrl = WebViewUtil.getLoadedUrl(twitterWebView); + if (Link.isValidLink(loadedUrl.toExternalForm())) { + String completeUrl = BrowserPanel.parseUrl(loadedUrl.toExternalForm()); + try { + loadedUrl = new URL(completeUrl); + return loadedUrl; + } catch (MalformedURLException mue) { + throw new AssertionError("URL expected to be valid."); + } + } + return WebViewUtil.getLoadedUrl(facebookWebView); + } + + /** + * Returns the {@code URL} of the currently loaded page for the specified {@code browserTab}. + */ + public URL getLoadedUrl(String browserTab) { + if (browserTab.equals(Link.TWITTER_LINK_TYPE)) { + URL loadedUrl = WebViewUtil.getLoadedUrl(twitterWebView); + if (Link.isValidLink(loadedUrl.toExternalForm())) { + String completeUrl = BrowserPanel.parseUrl(loadedUrl.toExternalForm()); + try { + loadedUrl = new URL(completeUrl); + return loadedUrl; + } catch (MalformedURLException mue) { + throw new AssertionError("URL expected to be valid."); + } + } + } + return getLoadedUrl(); + } + +``` +###### \java\seedu\address\logic\commands\AddPlatformCommandTest.java +``` java +public class AddPlatformCommandTest { + public static final String LINK_STUB = "www.facebook.com/carl.kz"; + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_invalidPlatformLink_failure() { + String invalidLink = "www.google.com"; + Map smpMap = new HashMap<>(); + smpMap.put(Link.FACEBOOK_LINK_TYPE, new Facebook(new Link(invalidLink))); + + AddPlatformCommand addPlatformCommand = prepareCommand(INDEX_THIRD_PERSON, smpMap); + + assertCommandFailure(addPlatformCommand, model, SocialMediaPlatformFactory.MESSAGE_BUILD_ERROR); + } + + @Test + public void execute_addPlatformUnfilteredList_success() throws Exception { + Person thirdPerson = model.getFilteredPersonList().get(INDEX_THIRD_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(thirdPerson).withPlatforms(LINK_STUB).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_THIRD_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(thirdPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_retainAndAddPlatformsUnfilteredList_success() throws Exception { + Person secondPerson = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + Set linkSet = new LinkedHashSet<>(); + linkSet.add(LINK_STUB); + for (String key : secondPerson.getSocialMediaPlatformMap().keySet()) { + linkSet.add(secondPerson.getSocialMediaPlatformMap().get(key).getLink().value); + } + Person editedPerson = new PersonBuilder(secondPerson) + .withPlatforms(linkSet.toArray(new String[linkSet.size()])).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_SECOND_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(secondPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_clearAllPlatformsUnfilteredList_success() throws Exception { + Person thirdPerson = model.getFilteredPersonList().get(INDEX_THIRD_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(thirdPerson).withPlatforms("").build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_THIRD_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = + String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(thirdPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_addPlatformFilteredList_success() throws Exception { + showPersonAtIndex(model, INDEX_THIRD_PERSON); + + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())) + .withPlatforms(LINK_STUB).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_FIRST_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(firstPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_BOB); + + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book. + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + + //Ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_AMY); + + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeUndoRedo_validIndexUnfilteredList_success() throws Exception { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms(LINK_STUB).build(); + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_FIRST_PERSON, editedPerson.getSocialMediaPlatformMap()); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + // addplatform -> first person platforms changed + addPlatformCommand.execute(); + undoRedoStack.push(addPlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + // redo -> same first person modified again + expectedModel.updatePerson(personToEdit, editedPerson); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_invalidIndexUnfilteredList_failure() { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_AMY); + + // execution failed -> addPlatformCommand not pushed into undoRedoStack + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + + // no commands in undoRedoStack -> undoCommand and redoCommand fail + assertCommandFailure(undoCommand, model, UndoCommand.MESSAGE_FAILURE); + assertCommandFailure(redoCommand, model, RedoCommand.MESSAGE_FAILURE); + } + + /** + * 1. Modifies a person's social media platform from a filtered list. + * 2. Undo the modification. + * 3. The unfiltered list should be shown now. Verify that the index of the previously modified person in the + * unfiltered list is different from the index at the filtered list. + * 4. Redo the modification. This ensures {@code RedoCommand} modifies the person object regardless of indexing. + */ + @Test + public void executeUndoRedo_validIndexFilteredList_samePersonModified() throws Exception { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Set linkSet = new LinkedHashSet<>(); + linkSet.add(LINK_STUB); + for (String key : firstPerson.getSocialMediaPlatformMap().keySet()) { + linkSet.add(firstPerson.getSocialMediaPlatformMap().get(key).getLink().toString()); + } + + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + AddPlatformCommand addPlatformCommand = prepareCommand(INDEX_FIRST_PERSON, + SampleDataUtil.getSocialMediaPlatformMap(linkSet.toArray(new String[linkSet.size()]))); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + showPersonAtIndex(model, INDEX_SECOND_PERSON); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit) + .withPlatforms(linkSet.toArray(new String[linkSet.size()])).build(); + + // addplatform -> modifies second person in unfiltered person list / first person in filtered person list + addPlatformCommand.execute(); + undoRedoStack.push(addPlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + expectedModel.updatePerson(personToEdit, editedPerson); + assertNotEquals(personToEdit, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + // redo -> modifies same second person in unfiltered person list + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equals() { + Map amyLinkMap = new HashMap<>(); + Map bobLinkMap = new HashMap<>(); + for (String key : SMP_MAP_AMY.keySet()) { + amyLinkMap.put(key, SMP_MAP_AMY.get(key).getLink()); + } + for (String key : SMP_MAP_BOB.keySet()) { + amyLinkMap.put(key, SMP_MAP_BOB.get(key).getLink()); + } + + final AddPlatformCommand standardCommand = new AddPlatformCommand(INDEX_FIRST_PERSON, amyLinkMap); + + // same values -> returns true + AddPlatformCommand commandWithSameValues = new AddPlatformCommand(INDEX_FIRST_PERSON, amyLinkMap); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new AddPlatformCommand(INDEX_SECOND_PERSON, amyLinkMap))); + + // different remark -> returns false + assertFalse(standardCommand.equals(new AddPlatformCommand(INDEX_FIRST_PERSON, bobLinkMap))); + } + + /** + * Returns an {@code AddPlatformCommand} with parameters {@code index} and {@code Map}. + */ + private AddPlatformCommand prepareCommand(Index index, Map smpMap) { + Map linkMap = new HashMap<>(); + for (String key : smpMap.keySet()) { + linkMap.put(key, smpMap.get(key).getLink()); + } + AddPlatformCommand addPlatformCommand = new AddPlatformCommand(index, linkMap); + addPlatformCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + return addPlatformCommand; + } +} +``` +###### \java\seedu\address\logic\commands\RemovePlatformCommandTest.java +``` java +/** + * Contains integration tests (interaction with the Model, UndoCommand and RedoCommand) and unit tests for + * {@code RemovePlatformCommand}. + */ +public class RemovePlatformCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_removePlatformsWithPlatformFields_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.FACEBOOK_LINK_TYPE); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithPlatformFieldsDifferentCasing_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.FACEBOOK_LINK_TYPE.toUpperCase()); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithSomeUnrecognisedPlatformFields_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add("random"); + platformSet.add(Link.FACEBOOK_LINK_TYPE); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithAllUnrecognisedPlatformFields_failure() { + Set platformSet = new HashSet<>(); + platformSet.add(""); + platformSet.add("hello"); + platformSet.add("tester"); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + assertCommandFailure(removePlatformCommand, model, RemovePlatformCommand.MESSAGE_PLATFORM_MAP_NOT_EDITED); + } + + @Test + public void execute_validIndexUnfilteredList_success() throws Exception { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() throws Exception { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + //Update expectedModel to be filtered + String[] splitName = personToEdit.getName().fullName.split("\\s+"); + Predicate predicate = new NameContainsKeywordsPredicate(Arrays.asList(splitName[0])); + expectedModel.updateFilteredPersonList(predicate); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeUndoRedo_validIndexUnfilteredList_success() throws Exception { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + // removeplatform -> first person's platforms removed + removePlatformCommand.execute(); + undoRedoStack.push(removePlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + // redo -> same first person's platforms deleted again + expectedModel.updatePerson(personToEdit, editedPerson); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_invalidIndexUnfilteredList_failure() { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + // execution failed -> removePlatformCommand not pushed into undoRedoStack + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + + // no commands in undoRedoStack -> undoCommand and redoCommand fail + assertCommandFailure(undoCommand, model, UndoCommand.MESSAGE_FAILURE); + assertCommandFailure(redoCommand, model, RedoCommand.MESSAGE_FAILURE); + } + + /** + * 1. Modifies a {@code Person} from a filtered list by removing the stated social media platform. + * 2. Undo the modification. + * 3. The unfiltered list should be shown now. Verify that the index of the previously modified person in the + * unfiltered list is different from the index at the filtered list. + * 4. Redo the modification. This ensures {@code RedoCommand} modifies the person object regardless of indexing. + */ + @Test + public void executeUndoRedo_validIndexFilteredList_samePersonModified() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.TWITTER_LINK_TYPE); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + showPersonAtIndex(model, INDEX_SECOND_PERSON); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + // removeplatform -> removes the Twitter platform from the second person in unfiltered person list / + // first person in filtered person list + removePlatformCommand.execute(); + undoRedoStack.push(removePlatformCommand); + + // undo -> reverts address book back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + expectedModel.updatePerson(personToEdit, editedPerson); + assertNotEquals(personToEdit, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + // redo -> edits the same second person in unfiltered person list and removing the Twitter platform + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equals() { + RemovePlatformCommand removePlatformFirstCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + RemovePlatformCommand removePlatformSecondCommand = prepareCommand(INDEX_SECOND_PERSON, new HashSet<>()); + + // same object -> returns true + assertTrue(removePlatformFirstCommand.equals(removePlatformFirstCommand)); + + // same values -> returns true + RemovePlatformCommand removePlatformFirstCommandCopy = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + assertTrue(removePlatformFirstCommand.equals(removePlatformFirstCommandCopy)); + + // different types -> returns false + assertFalse(removePlatformFirstCommand.equals(1)); + + // null -> returns false + assertFalse(removePlatformFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(removePlatformFirstCommand.equals(removePlatformSecondCommand)); + } + + /** + * Returns a {@code RemovePlatformCommand} with the parameters {@code index} and {@code platformSet}. + */ + private RemovePlatformCommand prepareCommand(Index index, Set platformSet) { + RemovePlatformCommand removePlatformCommand = new RemovePlatformCommand(index, platformSet); + removePlatformCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + return removePlatformCommand; + } + +} +``` +###### \java\seedu\address\logic\commands\SortCommandTest.java +``` java +/** + * Contains integration tests (interaction with the Model) and unit tests for SortCommand. + */ +public class SortCommandTest { + + private Model model; + private Model customModel; + private Model expectedModel; + private SortCommand sortCommand; + + @Before + public void setUp() throws Exception { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + customModel = new ModelManager(generateModelWithPersons(generatePersonList( + TypicalPersons.JOHN3, TypicalPersons.JOHN2, TypicalPersons.JANE, TypicalPersons.BLAKE, + TypicalPersons.HOB2, TypicalPersons.JOHN1, TypicalPersons.LEONARD, TypicalPersons.HOB1 + )).getAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(generateModelWithPersons(generatePersonList( + TypicalPersons.BLAKE, TypicalPersons.HOB1, TypicalPersons.HOB2, TypicalPersons.JANE, + TypicalPersons.JOHN1, TypicalPersons.JOHN2, TypicalPersons.JOHN3, TypicalPersons.LEONARD + )).getAddressBook(), new UserPrefs()); + sortCommand = new SortCommand(); + sortCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + } + + @Test + public void execute_unfilteredListAlreadySorted_failure() { + assertCommandFailure(sortCommand, model, SortCommand.MESSAGE_FAILURE); + } + + @Test + public void execute_filteredListAlreadySorted_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + assertCommandFailure(sortCommand, model, SortCommand.MESSAGE_FAILURE); + } + + @Test + public void execute_emptyList_failure() { + model.resetData(new AddressBook()); + assertCommandFailure(sortCommand, model, Messages.MESSAGE_ADDRESS_BOOK_EMPTY); + } + + @Test + public void execute_unfilteredListUnsorted_success() { + model.resetData(customModel.getAddressBook()); + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_filteredListUnsorted_success() { + String[] keywords = {"jane", "blake", "hob"}; + Predicate predicate = new NameContainsKeywordsPredicate(Arrays.asList(keywords)); + + model.resetData(customModel.getAddressBook()); + model.updateFilteredPersonList(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_unfilteredList_success() throws Exception { + model.resetData(customModel.getAddressBook()); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + + // sort -> sorts all persons in address book + sortCommand.execute(); + undoRedoStack.push(sortCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, customModel); + + // redo -> sorts all persons in address book again + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + /** + * 1. Sorts all persons from a filtered list, such that the list is kept. + * 2. Undo the sorting. + * 3. The unfiltered list should be shown now. Verify that the list is reverted to before it is sorted. + * 4. Redo the sorting. The list shown should still be unfiltered. + */ + @Test + public void executeUndoRedo_filteredList_success() { + model.resetData(customModel.getAddressBook()); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + + showPersonAtIndex(model, INDEX_THIRD_PERSON); + showPersonAtIndex(expectedModel, Index.fromZeroBased( + expectedModel.getFilteredPersonList().indexOf(model.getFilteredPersonList().get(0)))); + + // sort -> sorts all persons in address book + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + undoRedoStack.push(sortCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, customModel); + + // redo -> sorts all persons in address book again + expectedModel.updateFilteredPersonList(unused -> true); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + +``` +###### \java\seedu\address\logic\commands\SortCommandTest.java +``` java + /** + * Creates a model with all persons found in the list added. + */ + private Model generateModelWithPersons(List personList) throws Exception { + Model m = new ModelManager(); + for (Person p : personList) { + m.addPerson(p); + } + return m; + } +} +``` +###### \java\seedu\address\logic\parser\AddPlatformCommandParserTest.java +``` java +public class AddPlatformCommandParserTest { + + private static final String FACEBOOK_LINK_FIELD_AMY = " " + PREFIX_LINK + VALID_FACEBOOK_LINK_AMY; + private static final String TWITTER_LINK_FIELD_AMY = " " + PREFIX_LINK + VALID_TWITTER_LINK_AMY; + private static final String TWITTER_LINK_FIELD_BOB = " " + PREFIX_LINK + VALID_TWITTER_LINK_BOB; + + private static final String INVALID_LINK_1 = " " + PREFIX_LINK + "www.google.com"; + private static final String INVALID_LINK_2 = " " + PREFIX_LINK + "www.facebook.com"; + private static final String INVALID_LINK_3 = " " + PREFIX_LINK + "www.twitter.com"; + + private static final String LINK_EMPTY = " " + PREFIX_LINK; + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPlatformCommand.MESSAGE_USAGE); + + private AddPlatformCommandParser parser = new AddPlatformCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_FACEBOOK_LINK_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", AddPlatformCommand.MESSAGE_LINK_COLLECTION_EMPTY); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + FACEBOOK_LINK_FIELD_AMY, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + FACEBOOK_LINK_FIELD_AMY, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_LINK_1, Link.MESSAGE_INVALID_LINK); // unrecognised link + assertParseFailure(parser, "1" + INVALID_LINK_2, Link.MESSAGE_INVALID_LINK); // invalid facebook link + assertParseFailure(parser, "1" + INVALID_LINK_3, Link.MESSAGE_INVALID_LINK); // invalid twitter link + + // valid facebook link followed by invalid facebook link + assertParseFailure(parser, "1" + FACEBOOK_LINK_FIELD_AMY + INVALID_LINK_2, Link.MESSAGE_INVALID_LINK); + // invalid facebook link followed by valid facebook link + assertParseFailure(parser, "1" + INVALID_LINK_2 + FACEBOOK_LINK_FIELD_AMY, Link.MESSAGE_INVALID_LINK); + + // valid twitter link followed by invalid twitter link + assertParseFailure(parser, "1" + TWITTER_LINK_FIELD_AMY + INVALID_LINK_3, Link.MESSAGE_INVALID_LINK); + // invalid twitter link followed by valid twitter link + assertParseFailure(parser, "1" + INVALID_LINK_3 + TWITTER_LINK_FIELD_AMY, Link.MESSAGE_INVALID_LINK); + + //multiple empty link fields + assertParseFailure(parser, "1" + LINK_EMPTY + LINK_EMPTY, Link.MESSAGE_INVALID_LINK); + } + + @Test + public void parse_multipleLinksForSamePlatform_failure() { + assertParseFailure(parser, + "1" + TWITTER_LINK_FIELD_AMY + TWITTER_LINK_FIELD_BOB, Link.MESSAGE_LINK_CONSTRAINTS); + } + + @Test + public void parse_oneLinkFieldSpecified_success() { + Index targetIndex = INDEX_FIRST_PERSON; + String userInput = targetIndex.getOneBased() + FACEBOOK_LINK_FIELD_AMY; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(VALID_FACEBOOK_LINK_AMY), new Link(VALID_FACEBOOK_LINK_AMY)); + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + TWITTER_LINK_FIELD_AMY; + linkMap.clear(); + linkMap.put(Link.getLinkType(VALID_TWITTER_LINK_AMY), new Link(VALID_TWITTER_LINK_AMY)); + expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleLinkFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND_PERSON; + String userInput = targetIndex.getOneBased() + FACEBOOK_LINK_FIELD_AMY + TWITTER_LINK_FIELD_AMY; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(VALID_FACEBOOK_LINK_AMY), new Link(VALID_FACEBOOK_LINK_AMY)); + linkMap.put(Link.getLinkType(VALID_TWITTER_LINK_AMY), new Link(VALID_TWITTER_LINK_AMY)); + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_clearPlatforms_success() { + Index targetIndex = INDEX_THIRD_PERSON; + String userInput = targetIndex.getOneBased() + LINK_EMPTY; + + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, Collections.emptyMap()); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} +``` +###### \java\seedu\address\logic\parser\AddressBookParserTest.java +``` java + AddCommand commandAlias = (AddCommand) parser.parseCommand(PersonUtil.getAddCommandAlias(person)); + assertEquals(new AddCommand(person), commandAlias); +``` +###### \java\seedu\address\logic\parser\AddressBookParserTest.java +``` java + @Test + public void parseCommand_sort() throws Exception { + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD) instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " 3") instanceof SortCommand); + } + + @Test + public void parseCommand_addPlatform() throws Exception { + String testLink = "www.facebook.com/example"; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(testLink), new Link(testLink)); + AddPlatformCommand command = (AddPlatformCommand) parser.parseCommand(AddPlatformCommand.COMMAND_WORD + + " " + INDEX_FIRST_PERSON.getOneBased() + + " " + CliSyntax.PREFIX_LINK + testLink); + + assertEquals(new AddPlatformCommand(INDEX_FIRST_PERSON, linkMap), command); + } + + @Test + public void parseCommand_removePlatform() throws Exception { + String platform = "facebook"; + Set platformSet = new HashSet<>(); + platformSet.add(platform); + RemovePlatformCommand command = (RemovePlatformCommand) parser.parseCommand(RemovePlatformCommand.COMMAND_WORD + + " " + INDEX_FIRST_PERSON.getOneBased() + + " " + CliSyntax.PREFIX_SOCIAL_MEDIA_PLATFORM + platform); + + assertEquals(new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet), command); + } + + @Test + public void parseCommand_search() throws Exception { + String searchName = "foo"; + SearchCommand command = (SearchCommand) parser.parseCommand( + SearchCommand.COMMAND_WORD + " " + searchName); + assertEquals(new SearchCommand("all", searchName), command); + } +``` +###### \java\seedu\address\logic\parser\RemovePlatformCommandParserTest.java +``` java +public class RemovePlatformCommandParserTest { + + private static final String FACEBOOK_PLATFORM_FIELD = " " + PREFIX_SOCIAL_MEDIA_PLATFORM + Link.FACEBOOK_LINK_TYPE; + private static final String TWITTER_PLATFORM_FIELD = " " + PREFIX_SOCIAL_MEDIA_PLATFORM + Link.TWITTER_LINK_TYPE; + + private RemovePlatformCommandParser parser = new RemovePlatformCommandParser(); + private Set platformSet = new HashSet<>(); + + @Test + public void parse_validArgsNoPlatformFieldsSpecified_returnsRemovePlatformCommand() { + assertParseSuccess(parser, "1", new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet)); + } + + @Test + public void parse_validArgsPlatformFieldsSpecified_returnsRemovePlatformCommand() { + platformSet.add(Link.FACEBOOK_LINK_TYPE); + platformSet.add(Link.TWITTER_LINK_TYPE); + assertParseSuccess(parser, "1" + FACEBOOK_PLATFORM_FIELD + TWITTER_PLATFORM_FIELD, + new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemovePlatformCommand.MESSAGE_USAGE)); + } +} +``` +###### \java\seedu\address\logic\parser\SearchCommandParserTest.java +``` java +public class SearchCommandParserTest { + + private static final String INVALID_SEARCH_NAME = "a%b2$c"; + private static final String VALID_SEARCH_NAME = "abc"; + private static final String TWITTER_PLATFORM = "twitter"; + private static final String FACEBOOK_PLATFORM_ALIAS = "fb"; + + private SearchCommandParser parser = new SearchCommandParser(); + + @Test + public void parse_noPlatformSpecifiedInvalidSearchName_failure() { + assertParseFailure(parser, INVALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_noPlatformSpecifiedValidSearchName_success() { + assertParseSuccess(parser, VALID_SEARCH_NAME, new SearchCommand("all", VALID_SEARCH_NAME)); + } + + @Test + public void parse_validPlatformSpecifiedInvalidSearchName_failure() { + assertParseFailure(parser, TWITTER_PLATFORM + ", " + INVALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validPlatformSpecifiedValidSearchName_success() { + assertParseSuccess(parser, FACEBOOK_PLATFORM_ALIAS + ", " + VALID_SEARCH_NAME, + new SearchCommand(FACEBOOK_PLATFORM_ALIAS, VALID_SEARCH_NAME)); + } + + @Test + public void parse_invalidPlatformSpecifiedValidSearchName_failure() { + String invalidPlatform = "aha"; + assertParseFailure(parser, invalidPlatform + ", " + VALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "yo, test, this, command", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } +} +``` +###### \java\seedu\address\model\smplatform\LinkTest.java +``` java +public class LinkTest { + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Link(null)); + } + + @Test + public void isValidLink() { + // null link + Assert.assertThrows(NullPointerException.class, () -> Link.isValidLink(null)); + + // invalid links + assertFalse(Link.isValidLink("")); // empty string + assertFalse(Link.isValidLink(" ")); // spaces only + assertFalse(Link.isValidLink("www.google.com/")); // unknown link + assertFalse(Link.isValidLink("www.facebook.com/")); // facebook link without any path specified + assertFalse(Link.isValidLink("www.facebook.com////")); // facebook link with slashes only + assertFalse(Link.isValidLink("www.facebook.com/ /")); // facebook link with space as profile username + assertFalse(Link.isValidLink("www.twitter.com/")); // twitter link without any path specified + assertFalse(Link.isValidLink("www.twitter.com////")); // twitter link with slashes only + assertFalse(Link.isValidLink("www.twitter.com/ /")); // twitter link with space as username handle + + // valid links + assertTrue(Link.isValidLink("https://www.facebook.com/abc")); // facebook page with protocol and subdomain + assertTrue(Link.isValidLink("http://facebook.com/abc")); // facebook page with protocol only + assertTrue(Link.isValidLink("www.facebook.com/teo.yong")); // facebook page with subdomain only + assertTrue(Link.isValidLink("facebook.com/abc")); // facebook page with profile username only + assertTrue(Link.isValidLink("facebook.com/profile.php?id=100008354955053")); // facebook page with ID + assertTrue(Link.isValidLink("https://www.twitter.com/abc")); // twitter page with protocol and subdomain + assertTrue(Link.isValidLink("http://twitter.com/__ChrisLee")); // twitter page with protocol only + assertTrue(Link.isValidLink("www.twitter.com/yosp")); // twitter page with subdomain only + assertTrue(Link.isValidLink("twitter.com/abc")); // twitter page with username handle only + } +} +``` +###### \java\seedu\address\testutil\PersonUtil.java +``` java + /** + * Returns an add command alias string for adding the {@code person}. + */ + public static String getAddCommandAlias(Person person) { + return AddCommand.COMMAND_ALIAS + " " + getPersonDetails(person); + } + +``` +###### \java\seedu\address\ui\PersonCardTest.java +``` java + // with platforms + String[] links = {"www.facebook.com/examplepage", "www.twitter.com/examplepage"}; + Person personWithPlatforms = new PersonBuilder().withPlatforms(links).build(); + personCard = new PersonCard(personWithPlatforms, 3); + uiPartRule.setUiPart(personCard); + assertCardDisplay(personCard, personWithPlatforms, 3); + + // with platforms purposely put into wrong key, should not display any icons + Map customSmpMap = new HashMap(); + customSmpMap.put(Link.UNKNOWN_LINK_TYPE, new Facebook(new Link("www.facebook.com/testlink"))); + Set defaultTags = new HashSet<>(); + defaultTags.add(new Tag("friends")); + + Person personWithIncorrectSmpMap = new Person( + new Name("Alice Pauline"), new Phone("85355255"), + new Email("alice@gmail.com"), new Address("123, Jurong West Ave 6, #08-111"), + customSmpMap, defaultTags); + personCard = new PersonCard(personWithIncorrectSmpMap, 4); + uiPartRule.setUiPart(personCard); + assertCardDisplay(personCard, personWithIncorrectSmpMap, 4); +``` +###### \java\systemtests\AddressBookSystemTest.java +``` java + /** + * Asserts that the browser's url is changed to display the details of the person in the person list panel at + * {@code expectedSelectedCardIndex}, and only the card at {@code expectedSelectedCardIndex} is selected. + * @see BrowserPanelHandle#isUrlChanged() + * @see PersonListPanelHandle#isSelectedPersonCardChanged() + */ + protected void assertSelectedCardChanged(Index expectedSelectedCardIndex) { + String selectedBrowserLink = ""; + Map selectedPersonSmpMap = getModel().getFilteredPersonList().get( + expectedSelectedCardIndex.getZeroBased()).getSocialMediaPlatformMap(); + List keyList = new ArrayList<>(selectedPersonSmpMap.keySet()); + if (!keyList.isEmpty()) { + selectedBrowserLink = selectedPersonSmpMap.get(keyList.get(0)).getLink().value; + } + + URL expectedUrl; + URL actualUrl; + + try { + expectedUrl = getExpectedUrl(selectedBrowserLink); + actualUrl = getBrowserPanel().getLoadedUrl(Link.getLinkType(selectedBrowserLink)); + } catch (MalformedURLException mue) { + throw new AssertionError("URL expected to be valid."); + } + assertEquals(expectedUrl, actualUrl); + + assertEquals(expectedSelectedCardIndex.getZeroBased(), getPersonListPanel().getSelectedCardIndex()); + } + +``` +###### \java\systemtests\AddressBookSystemTest.java +``` java + /** + * Returns the expected URL in the correct format when provided with a String type {@code url}. + * {@code personName} is utilised when no URLs of the available platforms can be constructed. + */ + protected URL getExpectedUrl(String url) throws MalformedURLException { + if (Link.getLinkType(url).equals(Link.FACEBOOK_LINK_TYPE)) { + return new URL("https://m." + url.substring(url.indexOf(Link.FACEBOOK_LINK_TYPE))); + } else if (Link.getLinkType(url).equals(Link.TWITTER_LINK_TYPE)) { + return new URL("https://" + url); + } + + return MainApp.class.getResource(FXML_FILE_FOLDER + "default.html"); + } + +``` diff --git a/collated/test/shadow2496.md b/collated/test/shadow2496.md new file mode 100644 index 000000000000..33ce360bddd4 --- /dev/null +++ b/collated/test/shadow2496.md @@ -0,0 +1,336 @@ +# shadow2496 +###### \java\guitests\guihandles\ResultDisplayHandle.java +``` java + /** + * Returns the list of style classes present in the result display. + */ + public ObservableList getStyleClass() { + return getRootNode().getStyleClass(); + } +} +``` +###### \java\seedu\address\logic\commands\AddCommandTest.java +``` java + @Override + public void loginAccount(Account account) { + fail("This method should not be called."); + } + + @Override + public void setVerificationCode(String code) { + fail("This method should not be called."); + } + +``` +###### \java\seedu\address\logic\commands\LoginCommandTest.java +``` java +public class LoginCommandTest { + + @Test + public void execute_login_success() { + Account validAccount = new AccountBuilder().build(); + LoginCommand command = new LoginCommand(validAccount); + + String expectedMessage = String.format(LoginCommand.MESSAGE_SUCCESS, validAccount); + + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + command.setData(model, new CommandHistory(), new UndoRedoStack()); + + assertCommandSuccess(command, model, expectedMessage, model); + } + + @Test + public void equals() { + Account account = new AccountBuilder().withUsername(VALID_USERNAME_AMY) + .withPassword(VALID_PASSWORD_AMY).build(); + Account accountWithDiffUsername = new AccountBuilder().withUsername(VALID_USERNAME_BOB) + .withPassword(VALID_PASSWORD_AMY).build(); + Account accountWithDiffPassword = new AccountBuilder().withUsername(VALID_USERNAME_AMY) + .withPassword(VALID_PASSWORD_BOB).build(); + LoginCommand standardCommand = new LoginCommand(account); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // same values -> returns true + LoginCommand commandWithSameValues = new LoginCommand(account); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // different types -> returns false + assertFalse(standardCommand.equals(1)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different values -> returns false + LoginCommand commandWithDiffUsername = new LoginCommand(accountWithDiffUsername); + LoginCommand commandWithDiffPassword = new LoginCommand(accountWithDiffPassword); + assertFalse(standardCommand.equals(commandWithDiffUsername)); + assertFalse(standardCommand.equals(commandWithDiffPassword)); + } +} +``` +###### \java\seedu\address\logic\parser\AddressBookParserTest.java +``` java + @Test + public void parseCommand_login() throws Exception { + Account account = new AccountBuilder().build(); + LoginCommand command = (LoginCommand) parser.parseCommand( + LoginCommand.COMMAND_WORD + " " + AccountUtil.getAccountDetails(account)); + assertEquals(new LoginCommand(account), command); + } + +``` +###### \java\seedu\address\logic\parser\LoginCommandParserTest.java +``` java +public class LoginCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE); + + private LoginCommandParser parser = new LoginCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Account account = new AccountBuilder().withUsername(VALID_USERNAME_BOB) + .withPassword(VALID_PASSWORD_BOB).build(); + LoginCommand expectedCommand = new LoginCommand(account); + + // whitespace only preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + expectedCommand); + + // multiple usernames - last username accepted + assertParseSuccess(parser, USERNAME_DESC_AMY + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + expectedCommand); + + // multiple passwords - last password accepted + assertParseSuccess(parser, USERNAME_DESC_BOB + PASSWORD_DESC_AMY + PASSWORD_DESC_BOB, + expectedCommand); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + // missing username prefix + assertParseFailure(parser, VALID_USERNAME_BOB + PASSWORD_DESC_BOB, MESSAGE_INVALID_FORMAT); + + // missing password prefix + assertParseFailure(parser, USERNAME_DESC_BOB + VALID_PASSWORD_BOB, MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + // invalid username + assertParseFailure(parser, INVALID_USERNAME_DESC + PASSWORD_DESC_BOB, + Username.MESSAGE_USERNAME_CONSTRAINTS); + + // invalid password + assertParseFailure(parser, USERNAME_DESC_BOB + INVALID_PASSWORD_DESC, + Password.MESSAGE_PASSWORD_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, INVALID_USERNAME_DESC + INVALID_PASSWORD_DESC, + Username.MESSAGE_USERNAME_CONSTRAINTS); + } + + @Test + public void parse_invalidPreamble_failure() { + // non-empty preamble + assertParseFailure(parser, PREAMBLE_NON_EMPTY + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + MESSAGE_INVALID_FORMAT); + } +} +``` +###### \java\seedu\address\model\account\PasswordTest.java +``` java +public class PasswordTest { + + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Password(null)); + } + + @Test + public void constructor_invalidPassword_throwsIllegalArgumentException() { + String invalidPassword = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Password(invalidPassword)); + } + + @Test + public void isValidPassword() { + // null password + Assert.assertThrows(NullPointerException.class, () -> Password.isValidPassword(null)); + + // invalid passwords + assertFalse(Password.isValidPassword("")); // empty string + assertFalse(Password.isValidPassword(" ")); // spaces only + + // valid passwords + assertTrue(Password.isValidPassword("amy1111")); + assertTrue(Password.isValidPassword("-")); // one character + assertTrue(Password.isValidPassword("amy1111!very!very!very!long!example")); // long password + } +} +``` +###### \java\seedu\address\model\account\UsernameTest.java +``` java +public class UsernameTest { + + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Username(null)); + } + + @Test + public void constructor_invalidUsername_throwsIllegalArgumentException() { + String invalidUsername = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Username(invalidUsername)); + } + + @Test + public void isValidUsername() { + // null username + Assert.assertThrows(NullPointerException.class, () -> Username.isValidUsername(null)); + + // invalid usernames + assertFalse(Username.isValidUsername("")); // empty string + assertFalse(Username.isValidUsername(" ")); // spaces only + + // valid usernames + assertTrue(Username.isValidUsername("amybee")); + assertTrue(Username.isValidUsername("-")); // one character + assertTrue(Username.isValidUsername("amybee@very-very-very-long-example.com")); // long username + } +} +``` +###### \java\seedu\address\testutil\AccountBuilder.java +``` java +/** + * A utility class to help with building Account objects. + */ +public class AccountBuilder { + + private static final String DEFAULT_USERNAME = "alicepauline"; + private static final String DEFAULT_PASSWORD = "alice8535"; + + private Username username; + private Password password; + + public AccountBuilder() { + username = new Username(DEFAULT_USERNAME); + password = new Password(DEFAULT_PASSWORD); + } + + /** + * Sets the {@code Username} of the {@code Account} that we are building. + */ + public AccountBuilder withUsername(String username) { + this.username = new Username(username); + return this; + } + + /** + * Sets the {@code Password} of the {@code Account} that we are building. + */ + public AccountBuilder withPassword(String password) { + this.password = new Password(password); + return this; + } + + public Account build() { + return new Account(username, password); + } +} +``` +###### \java\seedu\address\testutil\AccountUtil.java +``` java +/** + * A utility class for Account. + */ +public class AccountUtil { + + /** + * Returns the part of command string for the given {@code account}'s details. + */ + public static String getAccountDetails(Account account) { + return PREFIX_USERNAME + account.getUsername().value + " " + + PREFIX_PASSWORD + account.getPassword().value; + } +} +``` +###### \java\seedu\address\ui\ResultDisplayTest.java +``` java + private static final NewResultAvailableEvent NEW_RESULT_EVENT_NON_ERROR = + new NewResultAvailableEvent("Non Error", false); + private static final NewResultAvailableEvent NEW_RESULT_EVENT_ERROR = + new NewResultAvailableEvent("Error", true); + + private ArrayList defaultStyleOfResultDisplay; + private ArrayList errorStyleOfResultDisplay; + +``` +###### \java\seedu\address\ui\ResultDisplayTest.java +``` java + uiPartRule.setUiPart(resultDisplay); + + defaultStyleOfResultDisplay = new ArrayList<>(resultDisplayHandle.getStyleClass()); + + errorStyleOfResultDisplay = new ArrayList<>(defaultStyleOfResultDisplay); + errorStyleOfResultDisplay.add(ResultDisplay.ERROR_STYLE_CLASS); + +``` +###### \java\seedu\address\ui\ResultDisplayTest.java +``` java + assertEquals(defaultStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); + + // new error result received + postNow(NEW_RESULT_EVENT_ERROR); + guiRobot.pauseForHuman(); + assertBehaviorForErrorResult(); + + // new non-error result received + postNow(NEW_RESULT_EVENT_NON_ERROR); + guiRobot.pauseForHuman(); + assertBehaviorForNonErrorResult(); + +``` +###### \java\seedu\address\ui\ResultDisplayTest.java +``` java + /** + * Verifies a result which has an error that
+ * - the text remains
+ * - the result display's style is the same as {@code errorStyleOfResultDisplay}. + */ + private void assertBehaviorForErrorResult() { + assertEquals(NEW_RESULT_EVENT_ERROR.message, resultDisplayHandle.getText()); + assertEquals(errorStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); + } + + /** + * Verifies a result which doesn't have an error that
+ * - the text remains
+ * - the result display's style is the same as {@code defaultStyleOfResultDisplay}. + */ + private void assertBehaviorForNonErrorResult() { + assertEquals(NEW_RESULT_EVENT_NON_ERROR.message, resultDisplayHandle.getText()); + assertEquals(defaultStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); + } +} +``` +###### \java\systemtests\AddressBookSystemTest.java +``` java + /** + * Asserts that the result display's shows the default style. + */ + protected void assertResultDisplayShowsDefaultStyle() { + assertEquals(RESULT_DISPLAY_DEFAULT_STYLE, getResultDisplay().getStyleClass()); + } + + /** + * Asserts that the result display's shows the error style. + */ + protected void assertResultDisplayShowsErrorStyle() { + assertEquals(RESULT_DISPLAY_ERROR_STYLE, getResultDisplay().getStyleClass()); + } + +``` diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index 0f0a8e7ab51e..85282aaf266f 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -3,53 +3,35 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + +Media Socializer was developed by the https://github.com/orgs/CS2103JAN2018-F12-B3/people[F12-B3] team. + {empty} + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] - -Role: Project Advisor - -''' - -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Seunghwan Choi +image::shadow2496.png[width="150", align="left"] +{empty}[https://github.com/shadow2496[github]] [<>] Role: Team Lead + -Responsibilities: UI - -''' - -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] - -Role: Developer + -Responsibilities: Data +Responsibilities: In charge of UI + Code quality + Integration ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Chuang Cheng Heng +image::kevinchuangch.png[width="150", align="left"] +{empty}[https://github.com/KevinChuangCH[github]] [<>] Role: Developer + -Responsibilities: Dev Ops + Threading +Responsibilities: In charge of Logic + Deliverables and deadlines + Scheduling and tracking ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Tan Wei Jie +image::nethergale.png[width="150", align="left"] +{empty}[https://github.com/Nethergale[github]] [<>] Role: Developer + -Responsibilities: UI +Responsibilities: In charge of Model and Storage + Documentation + Testing ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index eafdc9574a50..fadbaa1b8de1 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -1,6 +1,6 @@ = Contact Us :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103JAN2018-F12-B3/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] * *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 1733af113b29..49f909cf9e4e 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += Media Socializer - Developer Guide :toc: :toc-title: :toc-placement: preamble @@ -10,9 +10,9 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103JAN2018-F12-B3/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `Team F12-B3`      Since: `Jan 2018`      Licence: `MIT` == Setting up @@ -358,10 +358,238 @@ image::UndoRedoActivityDiagram.png[width="650"] ** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two * different things. // end::undoredo[] +// tag::findtag[] +=== Findtag feature +==== Current Implementation + +The 'findtag' feature is implemented with the same mechanism as the 'find' feature. It is used for finding stored persons with their tags instead of names. The key components of the feature are 'FindWithTagCommand', 'FindWithTagCommandParser', and 'TagContainsKeywordsPredicate'. + +The main component of 'findtag' is the 'FindWithTagCommand' which resides inside the logic component. It inherit from 'Command', as such, similar to 'find', 'findtag' is not an undoable command. From the class diagram of Logic class, we can see inheritance of 'FindWithTagCommand' in comparison with other types of commands: + +image::FindTagInLogicClassDiagram.png[width="800"] + +The user execute the FindWithTagCommand with the keyword ‘findtag’ following by the tag keywords, separated by whitespace. When the command is executed, the current list of persons shown in the GUI will only contain the persons with the tags same as the tag keywords. The following image shows the result of a 'findtag' execution: + +image::FindtagExecution.png[width="800"] + +Note that the tag keywords are not case sensitive; however, the command keyword is. For example, both 'findtag friends' and 'findtag FrIeNdS' can be executed and return a list of persons with the tag 'friends'. However 'FiNdTaG friends' cannot be executed and will result in a 'Unknown Command' feedback, as shown in the following: + +image::DifferentCaseSize.png[width="800"] + +Since the application does not recognize the command, the list of person displayed will not be changed. + +The 'findtag' command can handle multiple input tag keywords and, similar to 'find', 'findtag' is an ‘or’ search. For example, 'findtag friends owesMoney' will find any person with the tag ‘friends’ or ‘owesMoney’ or both. + +image::FindMultipleTags.png[width="800"] + +The following sequence diagram shows how the findtag operation works: + +image::FindTagSequenceDiagram.png[width="900"] + +==== Design Considerations + +===== Aspect: Implementation of `FindWithTagCommand` + +* **Alternative 1 (current choice):** Follow the mechanism of `FindCommand` +** Pros: 'findtag' works in the same mechanism as 'find', thus will not deviate from the overall architecture of the address book (i.e. Event Driven design). +** Cons: May generate classes/ methods that are implemented with very similar style, or in the worse case, we may have duplicate codes. Thus, additional attention needds to be given to ensure that the implementation of 'findtag' will not result in duplicate codes. +* **Alternative 2:** Implement new mechanism specifically for `FindWithTagCommand` +** Pros: Does not generate similar classes/ methods. +** Cons: Hard to implement, risk deviating away from the overall architecture of the address book. +// end::findtag[] + +// tag::sort[] +=== Sort feature +==== Current Implementation + +The sort mechanism is facilitated by the `SortCommand` and is used for sorting all persons found in the address book by name alphabetically. +It inherits from `UndoableCommand` and thus supports the undo/redo mechanism, enabling the ability to reverse the command and return the address book to its previous unsorted state. + +When the user executes the `SortCommand` using the command word `sort`, the current list of persons shown in the GUI will be retained. +As illustrated, a filtered list of persons will be displayed in a sorted manner once the command is executed: + +image::SortFilteredListDiagram.png[width="850"] + +{empty} + +The command can also handle multiple persons with similar names (i.e. different letter-casings) as shown below: + +image::SortSimilarNamesDiagram.png[width="650"] + +{empty} + +During sorting, names of two different persons are compared using the static method, `nameComparator`, found in the person class: +[source, java] +---- +public static Comparator nameComparator() { + return Comparator.comparing((Person p) -> p.getName().toString(), ( + s1, s2) -> (s1.compareToIgnoreCase(s2) == 0) ? s1.compareTo(s2) : s1.compareToIgnoreCase(s2)); +} +---- +As seen, casing is ignored when names are first compared with each other. The usual lexicographical order will instead be used when encountering identical names. + +The following sequence diagram shows how the sort operation works: + +image::SortCommandSequenceDiagram.png[width="800"] + +==== Design Considerations + +===== Aspect: Implementation of `SortCommand` +* **Alternative 1 (current choice):** Create a custom sorting method for person names +** Pros: Allows list to be displayed in a uniform manner, such that similar names are also sorted. +** Cons: Requires extra work to write custom method. +* **Alternative 2:** Utilise `String.CASE_INSENSITIVE_ORDER` for sorting +** Pros: Does not require additional work to write or override compare methods. +** Cons: List may be inconsistent when sorting multiple persons with similar names. + +===== Aspect: Implementation of Custom Sorting Method +* **Alternative 1 (current choice):** Use comparators and create one for person names called `nameComparator` +** Pros: Potentially enabling the ability to sort different fields other than names. +** Cons: May be more difficult to write. +* **Alternative 2:** Implement comparable and override the `compareTo` method in person class +** Pros: Simpler to understand. +** Cons: Can only define a single sorting sequence when comparing two different persons. + +===== Aspect: Display of person list in GUI +* **Alternative 1 (current choice):** Retain the current list of persons as shown in GUI but shown in a sorted manner +** Pros: User would not have to key in the command to filter the list after sorting. +** Cons: User may think that only the current list is sorted. +* **Alternative 2:** Displays the list of all persons found in the address book sorted alphabetically +** Pros: More properly describes what `sort` actually does by showing the full list of persons sorted. +** Cons: Current list is replaced by showing all persons. +// end::sort[] + +// tag::addplatform[] +=== Add Platform feature +==== Current Implementation + +The add platform function makes use of a factory to facilitate the creation of various social media platforms. +A platform is created only when users provide a valid profile link; this object will be associated to the particular +person found in the list as specified by the user. Currently, only the `Facebook` and `Twitter` platforms are available. + +The structure of the class diagram is as shown: + +image::SocialMediaPlatformFactoryClassDiagram.png[width="650"] + +As seen, a client can make use of the `SocialMediaPlatformFactory` to deal with the creation of social media platform +objects without the need of specifying the concrete class to construct. +Thus, it follows the https://www.tutorialspoint.com/design_pattern/factory_pattern.htm[factory pattern] since the +creation logic is abstracted away by the factory. The difference here to other implementations of the pattern is the +use of an _abstract_ class, `SocialMediaPlatform`, instead of the usual _interface_. + +In order to properly utilise the factory, a client needs to provide some form of specification: +[source, java] +---- +public final class SocialMediaPlatformFactory { + // ... class variables, constructor ... + + public static SocialMediaPlatform getSocialMediaPlatform(String type, Link link) throws IllegalValueException { + // ... social media platform instance creation logic ... + } +} +---- +From the extract, a `String` data type and a `Link` object need to be provided by the caller when using the +`getSocialMediaPlatform` method. The method will then return an instance of the available concrete classes. + +The `AddPlatformCommand` makes use of the implementation of `EditCommand` as the foundation, editing the `Person` model +to include the newly created social media platforms. Each platform is stored as an entry to the person's platform map, +with the key being the name of the platform. An example is shown below as an illustration: + +image::SocialMediaPlatformMapObjectDiagram.png[width="650"] + +{empty} + +The links provided by the user will be parsed within the `AddPlatformCommandParser` to convert them into `Link` +objects. From there, `AddPlatformCommand` will create a new map and add the relevant social media platforms by +calling the `getSocialMediaPlatform` method of `SocialMediaPlatformFactory` for object creation. To merge the updates +with the targeted person, the command would also retrieve all of its information in order to create a new `Person` +object before replacement is done. The sequence of the events is as follows: + +image::AddPlatformCommandSequenceDiagram.png[width="850"] + +{empty} + +In the UI Component, the `PersonCard` will be updated to show the icons of the platforms added. Suppose the user want +to add a `Facebook` profile to the first person. The `AddPlatformCommand` would be executed using the `addplatform` +command word. As illustration, the user has successfully added a `Facebook` platform to the targeted person by typing +`addplatform 4 l/www.facebook.com/david`: + +image::AddPlatformToPersonDiagram.png[width="750"] + +{empty} + +Since the basis of `AddPlatformCommand` is heavily inspired by the `EditCommand`, interactions between the various +components is exactly the same: + +image::AddPlatformCommandHighLevelSequenceDiagrams.png[width="800"] + +==== Design Considerations + +===== Aspect: Storing of Links in Person Model +* **Alternative 1 (current choice):** Create social media platform objects with the links as attributes and store into a map +** Pros: High potential of implementing methods for interactions between the actual profile page using the corresponding API. +** Cons: Accessing and inputting of data will be more complex. +* **Alternative 2:** Store using a set of links +** Pros: Ease of access to data. +** Cons: Can only be used for the displaying of pages, no room for other implementations. + +===== Aspect: Usage of Interface or Abstract Class +* **Alternative 1 (current choice):** Have `SocialMediaPlatform` be an abstract class +** Pros: Allows common methods to be used by classes that extends from it and still provide a means of ensuring critical methods that are abstract be implemented. +** Cons: Classes that want to make use of the implementations can only extend from it. +* **Alternative 2:** Have `SocialMediaPlatform` be an interface +** Pros: Higher flexibility for classes since they can implement many interfaces. +** Cons: Limitations in sharing and utilisation of default methods between classes. +// end::addplatform[] + +// tag::search[] +=== Search feature +==== Current Implementation + +The implementation of the search feature follows very closely to the Event-Driven architecture. It is used for searching profiles on the social media platforms available in the application. The key components of the feature is the 'SearchPersonEvent' and its handler method. + +The 'SearchCommand' class is located within the logic component. It is not an undoable command, hence it inherit from 'Command'. + +The search feature is able to perform search on either the input platform or all the available platforms. To execute the search feature, simply input the command keyword "search" followed by an optional declaration of platform to perform the search on, and lastly followed by the profile name to be searched for. + +image::SearchFunction.png[width="1000"] + +Similar to most of the other feature in the application, the command keyword is case sensitive while the search name is not. As for platform declaration, only predefined inputs are allowed. These predefined inputs are the names of the platform as well as the aliases. (e.g. facebook, fb, twitter...) An invalid command format prompt will be shown if the input platform is not one of the predefined inputs, or has a different case size. + +image::InvalidPlatformCases.png[width="1000"] + +Upon execution of the search feature with the keyword "search", the validation of input arguments are first checked by the parser, then a SearchPersonEvent will be posted to the event bus. This event is handled by the handleSearchPersonEvent method, which determine the platform to perform the search on and choose the tabs to display. + +image::SearchPersonEvent.png[width="800"] + +==== Design Considerations + +===== Aspect: Implementation of `SearchCommand` `SearchPersonEvent` and the handler method + +* **Alternative 1:** Implement multiple Command class, Event class, and handler method to facilitate searching on different social media platforms. +** Pros: Easier to implement since each class only need to handle search on one platform. +** Cons: High chance of generating similar/duplicated codes. +* **Alternative 2 (current choice):** Have a common 'SearchCommand' class for search. +** Pros: Reduce repeated/similar code. +** Cons: Harder to implement since the feature now has to determine the platform to search on base on the input form the user. + +===== Aspect: Link to the search functions of different social media platforms + +* **Alternative 1 (current choice):** Use the search URL of different platform to perfom the search. +** Pros: Much easier to implement since we are simply loading the search URL into our browser. +** Cons: Direct use of search URL uses a lot of resources. +* **Alternative 2:** Use a third party code to perform the search. +** Pros: May consume less resources. +** Cons: May have to program the UI to display the search result. + +// end::search[] + // tag::dataencryption[] === [Proposed] Data Encryption -_{Explain here how the data encryption feature will be implemented}_ +We are currently looking into the `javax.cipher` and `javax.security` packages for encryption of sensitive data. +The `Apache Commons Crypto` library has also been considered. Potentially, data encryption would be used for storing +personal profile information, such as social feeds and friend lists of contacts. // end::dataencryption[] @@ -777,19 +1005,65 @@ We now have everything set up... but we still can't modify the remarks. Let's fi See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. +// tag::product-scope[] [appendix] == Product Scope *Target user profile*: -* has a need to manage a significant number of contacts +* has a need to keep track a significant number of social media platforms of contacts +* has a need to view the latest scoop of various people's social life * prefer desktop apps over other types * can type fast * prefers typing over mouse input * is reasonably comfortable using CLI apps -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Value proposition*: one-stop access to multiple social media platforms + +*Feature Contribution*: + +[options="header"] +|======================================================================= +|Assignee |Major | Minor +|Seunghwan Choi +a| +* Updating the UI to show multiple social pages on the application window: +** With all the links to the different social media pages of a person stored in the user's address book, the user can +view all of the pages in a single window under collapsible tabs (to distinguish the different social media platform +pages) when he/she selects the particular person. +This simplifies the process of providing the user with his/her daily social media fix. +a| +* `login` function: +** Allows the user to login to all the available social media platforms as given by the application. +This feature is essential so that users can perform their required tasks, such as accessing their friends list and +viewing the social feeds of friends. + +|Chuang Cheng Heng +a| +* `search` function: +** Allows user to perform the search action on all the social media platforms available with a single command. +This feature improves the product significantly because a user can do search on the application instead of going to +the individual social media platform and repeatedly perform the search. +a| +* `findtag` command: +** Allows the user to find persons whose tags contain the input keywords. +This can assist users in filtering out people who only use a certain social media platform if they are tagged. + +|Tan Wei Jie +a| +* Manage social media platforms of persons function: +** Allows user to manage the social media platforms of each person through the `addplatform` and `removeplatform` +commands. +This feature allows a user to have the application keep track of all the links to the social media profiles of +each person in the address book so that he/she can easily access their profile pages when selected. +a| +* `sort` command: +** Allows the user to sort the address book list alphabetically. +This feature organises and arranges the list for easier viewing and searching. +|======================================================================= +// end::product-scope[] +// tag::user-stories[] [appendix] == User Stories @@ -806,12 +1080,70 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un |`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |user |see the page of the specified platform of a person |get information of the person with my preferred social media platform + +|`* * *` |user |delete one of social media platforms of a person |remove entries that I no longer need + +|`* * *` |user |find a person by name with the specified platforms |locate details of persons who have the social media platforms without having to go through the entire list + +|`* * *` |user |apply the filter of a social media platform |have a list of persons who are on the platform shown + +|`* * *` |user |apply multiple filters regarding social media platforms |have a list of persons who satisfy the filters shown + +|`* * *` |user |clear filters applied |get information regardless of them + +|`* * *` |social media user |select a person and see the social media platforms they are using |have the option not to go to the various platforms to check + +|`* * *` |social media user |store the links to the profiles of a person (friend) |have the app keep track of it for me + +|`* * *` |social media user |view the latest feed/status of a person |get up-to-date information about them + +|`* * *` |lazy social media user |log out from all my social media account with one command |remove potential unauthorised access effectively + +|`* * *` |social media user |log in to my account |access private profiles in my friend list + +|`* * *` |social media user |search for profile in all social media platforms with the same command | + +|`* * *` |social media user |send friend request from the app | + +|`* * *` |social media user |be able to react to a post (like, comment, etc) from the app directly | + +|`* * *` |lazy user |be able to remain logged in to my account | + +|`* * *` |social media user |post and update my statuses |let people know what I'm up to + |`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* *` |advanced user |use shorter versions of a command |type a command faster + +|`* *` |user |have the information of my social media accounts encrypted |keep my personal information safe + +|`* *` |user |add a new person with private contact details |keep them in secret + +|`* *` |user |search a person with the tags | + +|`* *` |careless user |logout from all my account remotely |ensure my account security will not be compromised in case I lose my device + +|`* *` |social media user |be able to customize what is displayed (profile, social media in use, or latest feed/status) when I select a person |personalise the app to my liking + +|`* *` |careful user |have an additional authentication system for editing my profile setting | + +|`* *` |social media user |be able to download files (e.g. images or documents) posted by others |keep a copy and view them offline + +|`* *` |social media user |send messages to the people on my friends list |communicate with them + +|`* *` |social media user |add people found in my friends list on social media to my address book |create contacts of them and communicate through other means + +|`* *` |social media user |find my friends that are near my location |meet up and have fun with them + +|`* *` |user |set up planned events with a group of my friends |remember their details and be punctual for them + +|`* *` |user |be reminded of important events (e.g. movie outings, birthday celebrations) |be prompted not to miss them + |`*` |user with many persons in the address book |sort persons by name |locate a person easily |======================================================================= -_{More to be added}_ +// end::user-stories[] [appendix] == Use Cases @@ -844,16 +1176,54 @@ Use case ends. + Use case resumes at step 2. +[discrete] +=== Use case: See the page of person + +*MSS* + +1. User requests to apply the filter of <> +2. User requests to list persons +3. AddressBook shows a list of persons with the filter applied +4. User requests to see the page of a specific person in the list +5. AddressBook shows the page of the specified platform ++ +Use case ends. + +*Extensions* + +[none] +* 1a. There is no filter available. ++ +Use case ends. + +* 3a. The list is empty. ++ +Use case ends. + +* 4a. The given index is invalid. ++ +[none] +** 4a1. AddressBook shows an error message. ++ +Use case resumes at step 3. + + _{More to be added}_ +// tag::non-functional-requirements[] [appendix] == Non Functional Requirements . Should work on any <> as long as it has Java `1.8.0_60` or higher installed. . Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. . A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +. Should display the social media used by the selected person fast. +. The login information of the user should be stored in a secure manner. +. Login to the user's account through the application should be equally fast as to directly login to the social media platform. +. The application should have an user friendly interface so that even users that are not familiar with CLI will have minimal problem navigating through the application. _{More to be added}_ +// end::non-functional-requirements[] [appendix] == Glossary @@ -864,6 +1234,9 @@ Windows, Linux, Unix, OS-X [[private-contact-detail]] Private contact detail:: A contact detail that is not meant to be shared with others +[[social-media-platform]] Social media platform:: +A platform that makes people distribute information and encourages them to connect with others + [appendix] == Product Survey diff --git a/docs/LearningOutcomes.adoc b/docs/LearningOutcomes.adoc deleted file mode 100644 index cf153ba8b38f..000000000000 --- a/docs/LearningOutcomes.adoc +++ /dev/null @@ -1,265 +0,0 @@ -= Learning Outcomes -:toc: macro -:toc-title: -:toclevels: 1 -:sectnums: -:sectnumlevels: 1 -:imagesDir: images -:stylesDir: stylesheets -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master - -After studying this code and completing the corresponding exercises, you should be able to, - -toc::[] - -''' - -== Use High-Level Designs `[LO-HighLevelDesign]` - -Note how the <> describes the high-level design using an _Architecture Diagrams_ and high-level sequence diagrams. - -*Resources* - -* https://se-edu.github.io/se-book/architecture/[se-edu/se-book: Design: Architecture] -* https://se-edu.github.io/se-book/design/introduction/multilevelDesign/[se-edu/se-book: Design: Introduction: Multi-Level Design] - -''' - -== Use Event-Driven Programming `[LO-EventDriven]` - -Note how the <> uses events to communicate with components without needing a direct coupling. Also note how the link:{repoURL}/src/main/java/seedu/address/commons/core/index/EventsCenter.java[`EventsCenter.java`] acts as an event dispatcher to facilitate communication between event creators and event consumers. - -*Resources* - -* https://se-edu.github.io/se-book/architecture/architecturalStyles/eventDriven/[se-edu/se-book: Design: Architecture: Architecture Styles: Event-Driven Architectural Style] - -''' - -== Use API Design `[LO-ApiDesign]` - -Note how components of AddressBook have well-defined APIs. For example, the API of the `Logic` component is given in the link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -image:LogicClassDiagram.png[width="800"] - -*Resources* - -* https://se-edu.github.io/se-book/reuse/apis/[se-edu/se-book: Implementation: Reuse: APIs] - -''' - -== Use Assertions `[LO-Assertions]` - -Note how the AddressBook app uses Java ``assert``s to verify assumptions. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/assertions/[se-edu/se-book: Implementation: Error Handling: Assertions] - -=== Exercise: Add more assertions - -* Make sure assertions are enabled in your IDE by forcing an assertion failure (e.g. add `assert false;` somewhere in the code and run the code to ensure the runtime reports an assertion failure). -* Add more assertions to AddressBook as you see fit. - - -''' - -== Use Logging `[LO-Logging]` - -Note <>. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/logging/[se-edu/se-book: Implementation: Error Handling: Logging] - -=== Exercise: Add more logging - -Add more logging to AddressBook as you see fit. - - -''' - -== Use Defensive Coding `[LO-DefensiveCoding]` - -Note how AddressBook uses the `ReadOnly*` interfaces to prevent objects being modified by clients who are not supposed to modify them. - -*Resources* - -* https://se-edu.github.io/se-book/errorHandling/defensiveProgramming/[se-edu/se-book: Implementation: Error Handling: Defensive Programming] - -=== Exercise: identify more places for defensive coding - -Analyze the AddressBook code/design to identify, - -* where defensive coding is used -* where the code can be more defensive - -''' - -== Use Build Automation `[LO-BuildAutomation]` - -Note <>. - -*Resources* - -* https://se-edu.github.io/se-book/integration/buildAutomation/what/[se-edu/se-book: Implementation: Integration: Build Automation: What] - -=== Exercise: Use gradle to run tasks - -* Use gradle to do these tasks: Run all tests in headless mode, build the jar file. - -=== Exercise: Use gradle to manage dependencies - -* Note how the build script `build.gradle` file manages third party dependencies such as ControlsFx. Update that file to manage a third-party library dependency. - - -''' - -== Use Continuous Integration `[LO-ContinuousIntegration]` - -Note <>. (https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]]) - -*Resources* - -* https://se-edu.github.io/se-book/integration/buildAutomation/continuousIntegrationDeployment/[se-edu/se-book: Implementation: Integration: Build Automation: CI & CD] - -=== Exercise: Use Travis in your own project - -* Set up Travis to perform CI on your own fork. - - -''' - -== Use Code Coverage `[LO-CodeCoverage]` - -Note how our CI server <>. (https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]]) After <> for your project, you can visit Coveralls website to find details about the coverage of code pushed to your repo. https://coveralls.io/github/se-edu/addressbook-level4?branch=master[Here] is an example. - -*Resources* - -* https://se-edu.github.io/se-book/testing/testCoverage/[se-edu/se-book: QA: Testing: Test Coverage] - -=== Exercise: Use the IDE to measure coverage locally - -* Use the IDE to measure code coverage of your tests. - -''' - -== Apply Test Case Design Heuristics `[LO-TestCaseDesignHeuristics]` - -The link:{repoURL}/src/test/java/seedu/address/commons/util/StringUtilTest.java[`StringUtilTest.java`] -class gives some examples of how to use _Equivalence Partitions_, _Boundary Value Analysis_, and _Test Input Combination Heuristics_ to improve the efficiency and effectiveness of test cases testing the link:../src/main/java/seedu/address/commons/util/StringUtil.java[`StringUtil.java`] class. - -*Resources* - -* https://se-edu.github.io/se-book/testCaseDesign/[se-edu/se-book: QA: Test Case Design] - -=== Exercise: Apply Test Case Design Heuristics to other places - -* Use the test case design heuristics mentioned above to improve test cases in other places. - -''' - -== Write Integration Tests `[LO-IntegrationTests]` - -Consider the link:{repoURL}/src/test/java/seedu/address/storage/StorageManagerTest.java[`StorageManagerTest.java`] class. - -* Test methods `prefsReadSave()` and `addressBookReadSave()` are integration tests. Note how they simply test if The `StorageManager` class is correctly wired to its dependencies. -* Test method `handleAddressBookChangedEvent_exceptionThrown_eventRaised()` is a unit test because it uses _dependency injection_ to isolate the SUT `StorageManager#handleAddressBookChangedEvent(...)` from its dependencies. - -Compare the above with link:{repoURL}/src/test/java/seedu/address/logic/LogicManagerTest.java[`LogicManagerTest`]. Some of the tests in that class (e.g. `execute_*` methods) are neither integration nor unit tests. They are _integration + unit_ tests because they not only check if the LogicManager is correctly wired to its dependencies, but also checks the working of its dependencies. For example, the following two lines test the `LogicManager` but also the `Parser`. - -[source,java] ----- -@Test -public void execute_invalidCommandFormat_throwsParseException() { - ... - assertParseException(invalidCommand, MESSAGE_UNKNOWN_COMMAND); - assertHistoryCorrect(invalidCommand); -} ----- - -*Resources* - -* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] - -=== Exercise: Write unit and integration tests for the same method. - -* Write a unit test for a high-level method somewhere in the code base (or a new method you wrote). -* Write an integration test for the same method. - -''' - -== Write System Tests `[LO-SystemTesting]` - -Note how tests below `src/test/java/systemtests` package (e.g link:{repoURL}/src/test/java/systemtests/AddCommandSystemTest.java[`AddCommandSystemTest.java`]) are system tests because they test the entire system end-to-end. - -*Resources* - -* https://se-edu.github.io/se-book/testing/testingTypes/[se-edu/se-book: QA: Testing: Testing Types] - -=== Exercise: Write more system tests - -* Write system tests for the new features you add. - -''' - -== Automate GUI Testing `[LO-AutomateGuiTesting]` - -Note how this project uses TextFX library to automate GUI testing, including <>. - -=== Exercise: Write more automated GUI tests - -* Covered by `[LO-SystemTesting]` - -''' - -== Apply Design Patterns `[LO-DesignPatterns]` - -Here are some example design patterns used in the code base. - -* *Singleton Pattern* : link:{repoURL}/src/main/java/seedu/address/commons/core/EventsCenter.java[`EventsCenter.java`] is Singleton class. Its single instance can be accessed using the `EventsCenter.getInstance()` method. -* *Facade Pattern* : link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager.java`] is not only shielding the internals of the Storage component from outsiders, it is mostly redirecting method calls to its internal components (i.e. minimal logic in the class itself). Therefore, `StorageManager` can be considered a Facade class. -* *Command Pattern* : The link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command.java`] and its sub classes implement the Command Pattern. -* *Observer Pattern* : The <> used by this code base employs the Observer pattern. For example, objects that are interested in events need to have the `@Subscribe` annotation in the class (this is similar to implementing an `\<>` interface) and register with the `EventsCenter`. When something noteworthy happens, an event is raised and the `EventsCenter` notifies all relevant subscribers. Unlike in the Observer pattern in which the `\<>` class is notifying all `\<>` objects, here the `\<>` classes simply raises an event and the `EventsCenter` takes care of the notifications. -* *MVC Pattern* : -** The 'View' part of the application is mostly in the `.fxml` files in the `src/main/resources/view` folder. -** `Model` component contains the 'Model'. However, note that it is possible to view the `Logic` as the model because it hides the `Model` behind it and the view has to go through the `Logic` to access the `Model`. -** Sub classes of link:{repoURL}/src/main/java/seedu/address/ui/UiPart.java[`UiPart`] (e.g. `PersonListPanel` ) act as 'Controllers', each controlling some part of the UI and communicating with the 'Model' (via the `Logic` component which sits between the 'Controller' and the 'Model'). -* *Abstraction Occurrence Pattern* : Not currently used in the app. - -*Resources* - -* https://se-edu.github.io/se-book/designPatterns/[se-edu/se-book: Design: Design Patterns] - -=== Exercise: Discover other possible applications of the patterns - -* Find other possible applications of the patterns to improve the current design. e.g. where else in the design can you apply the Singleton pattern? -* Discuss pros and cons of applying the pattern in each of the situations you found in the previous step. - -=== Exercise: Find more applicable patterns - -* Learn other _Gang of Four_ Design patterns to see if they are applicable to the app. - -''' - -== Use Static Analysis `[LO-StaticAnalysis]` - -Note how this project uses the http://checkstyle.sourceforge.net/[CheckStyle] static analysis tool to confirm compliance with the coding standard. - -*Resources* - -* https://se-edu.github.io/se-book/qualityAssurance/staticAnalysis/[se-edu/se-book: QA: Static Analysis] - -=== Exercise: Use CheckStyle locally to check style compliance - -* Install the CheckStyle plugin for your IDE and use it to check compliance of your code with our style rules (given in `/config/checkstyle/checkstyle.xml`). - -''' - -== Do Code Reviews `[LO-CodeReview]` - -* Note how some PRs in this project have been reviewed by other developers. Here is an https://github.com/se-edu/addressbook-level4/pull/147[example]. -* Also note how we have used https://www.codacy.com[Codacy] to do automate some part of the code review workload (https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]]) - - -=== Exercise: Review a PR - -* Review PRs created by team members. diff --git a/docs/LoadingPage.html b/docs/LoadingPage.html new file mode 100644 index 000000000000..06b7387add26 --- /dev/null +++ b/docs/LoadingPage.html @@ -0,0 +1,61 @@ + + + + + + Loading Page + + + +
+
+

Loading...

+
+ + diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 74248917e438..be37afea2c0d 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += Media Socializer - User Guide :toc: :toc-title: :toc-placement: preamble @@ -10,14 +10,15 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103JAN2018-F12-B3/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `Team F12-B3` Since: `Jan 2018` Licence: `MIT` == Introduction -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +Media Socializer (MeSo) is for those who *prefer to use a desktop app for managing contacts*. More importantly, MeSo is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, MeSo can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! == Quick Start @@ -38,7 +39,7 @@ e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. . Some example commands you can try: * *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. +* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Media Socializer. * **`delete`**`3` : deletes the 3rd contact shown in the current list * *`exit`* : exits the app @@ -60,9 +61,23 @@ e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. Format: `help` +=== Logging in : `login` `[Since v1.5]` + +Logs into a social media platform using the user's account information. + +Format: `login SOCIALMEDIA` + +**** +* A login window will pop up upon entering the command. +**** + +Examples: + +* `login facebook` + +Open the login window to facebook where the user can input his account and password. + === Adding a person: `add` -Adds a person to the address book + +Adds a person to the address book. + Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` [TIP] @@ -118,6 +133,28 @@ Returns `john` and `John Doe` * `find Betsy Tim John` + Returns any person having names `Betsy`, `Tim`, or `John` +// tag::findtag[] +=== Locating persons by tag: `findtag` `[Since v1.2]` + +Finds persons whose tags contain any of the given keywords. + +Format: `findtag KEYWORD [MORE_KEYWORDS]` + +**** +* The search is case insensitive. e.g `Friends` will match `friends` +* The order of the keywords does not matter. e.g. `friends classmate` will match `classmate friends` +* Only the tags are searched. +* Only full words will be matched e.g. `friend` will not match `friends` +* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `friends classmates` will return persons with tags `friends` and `neighbour`, `owesMoney` `friends` +**** + +Examples: + +* `findtag friends` + +Returns any person having tags `friends` +* `find friends classmate neighbour` + +Returns any person having tags `friends`, `classmate`, or `neighbour` +// end::findtag[] + === Deleting a person : `delete` Deletes the specified person from the address book. + @@ -138,13 +175,118 @@ Deletes the 2nd person in the address book. `delete 1` + Deletes the 1st person in the results of the `find` command. +// tag::addplatform[] +=== Adding social media platforms to a person : `addplatform` `[Since v1.3]` + +Adds social media platforms to an existing person in the address book by providing the associated profile links. + +Format: `addplatform INDEX [l/LINK]...` + +**** +* Adds the social media platforms by editing the person at the specified `INDEX`. The index refers to the index number shown in the last person listing. The index *must be a positive integer* 1, 2, 3, ... +* Existing values will be updated to the input values. +* During this operation, the existing platform links of the person that are unchanged will be retained i.e adding of platforms is cumulative. +* You may only store a single link for each social media platform. +* You can remove all the person's social media platforms by typing `l/` without specifying any links after it. +**** + +[NOTE] +======================================================================== +[halign="left",valign="top",options="header"] +|======================================================================= +|Valid Facebook Link Forms |Valid Twitter Link Forms +a| +* `\https://www.facebook.com/` +* `\https://facebook.com/` +* `\http://www.facebook.com/` +* `\http://facebook.com/` +* `www.facebook.com/` +* `facebook.com/` +* `facebook.com/profile.php?id=` +a| +* `\https://www.twitter.com/` +* `\https://twitter.com/` +* `\http://www.twitter.com/` +* `\http://twitter.com/` +* `www.twitter.com/` +* `twitter.com/` +|======================================================================= +======================================================================== + +IMPORTANT: It is not guaranteed that the link provided leads to an available page. Links are accepted so long as they +match any of the patterns as defined above. This feature will be improved to test for broken/removed pages +in future releases. + +Examples: + +* `addplatform 1 l/www.facebook.com/johndoe` + +Adds the Facebook platform with the link `www.facebook.com/johndoe` to the 1st person. +* `addplatform 2 l/` + +Clears all existing social media platforms of the 2nd person. +// end::addplatform[] + +// tag::removeplatform[] +=== Removing stored platforms of a person : `removeplatform` `[Since v1.4]` + +Removes the specified social media platforms of the stated person. + +Format: `removeplatform INDEX [smp/PLATFORM]...` + +**** +* Removes the social media platforms by editing the person at the specified `INDEX`. The index refers to the index number shown in the last person listing. The index *must be a positive integer* 1, 2, 3, ... +* You can remove all the social media platforms of a person by not specifying any platform fields. +* Unrecognised platforms will be ignored during execution. +**** + +Examples: + +* `removeplatform 1` + +Removes all social media platforms tied to the 1st person. +* `removeplatform 2 smp/twitter` + +Removes the Twitter platform from the 2nd person. +// end::removeplatform[] + +// tag::sort[] +=== Sorting all persons : `sort` `[Since v1.2]` + +Sorts all persons in the address book alphabetically and then displays the current list in sorted order. + +Format: `sort` +// end::sort[] + +// tag::search[] +=== Search for profile : `search` `[Since v1.5]` + +Search for the specified profiles on available social media platform within the application. + +Format: `search [PLATFORM,] KEYWORD` + +**** +* The PLATFORM is an optional input. It indicates the social media platform to perform the search on. +* If no PLATFORM is entered, the search will be performed on all platforms available. +* To indicate the platform, the user can enter the name of the platform (e.g. facebook) or use the inbuilt alias (e.g. fb) +* Currently available platform and the respective alias: +** 1) facebook (alias: fb) +** 2) twitter (alias: tw). +* The KEYWORD is the name to be searched for. +* Make use of the search function of the respective social media platform. +**** + +Examples: + +* `search john` +Search with keyword "john" on all the social media linked with the application and display the search result in the browser window under the respective tabs. + +* `search fb, tom` + +Search with keyword "tom" on Facebook and display the search result in the browser window under the Facebook tab. + +* `search twitter, tom` + +Search with keyword "tom" on Twitter and display the search result in the browser window under the Twitter tab. +// end::search[] + === Selecting a person : `select` Selects the person identified by the index number used in the last person listing. + Format: `select INDEX` **** -* Selects the person and loads the Google search page the person at the specified `INDEX`. +* Selects the person and loads the social media linked to the stored person at the specified `INDEX`. * The index refers to the index number shown in the most recent listing. * The index *must be a positive integer* `1, 2, 3, ...` **** @@ -176,7 +318,7 @@ Format: `undo` [NOTE] ==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +Undoable commands: those commands that modify the address book's content (`addplatform`, `removeplatform`, `add`, `delete`, `edit`, `sort` and `clear`). ==== Examples: @@ -234,11 +376,58 @@ Address book data are saved in the hard disk automatically after any command tha There is no need to save manually. // tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +=== Encrypting data files : `encrypt` `[Coming in v2.0]` + +Encrypt the specified person from the address book so that password will be required to view the information of the specified person. + +Format: `encrypt INDEX` -_{explain how the user can enable/disable data encryption}_ +**** +* Encrypts the person at the specified `INDEX`. +* The index refers to the index number shown in the most recent listing. +* The index *must be a positive integer* 1, 2, 3, ... +**** + +Examples: + +* `list` + +`encrypt 3` + +Encrypts the 3rd person in the address book. +* `find Betsy` + +`encrypt 1` + +Encrypts the 1st person in the results of the `find` command. // end::dataencryption[] +=== Make a post : `post` `[Coming in v2.0]` + +Publish a post on the specified social media. + +Format: `post SOCIALMEDIA/MESSAGE` + +**** +* User is required to login to their social media account. +* If not logged in, the application will prompt for the user to login. +**** + +Examples: + +* `post facebook/Sample post.` + +=== Logout from all the linked platform : `logoutall` `[Coming in v2.0]` + +Logout from all the social media platform that the user has logged in. + +Format: `logoutall` + +**** +* After this command, when the person is selected, the tab for the removed platform will not be shown. +* If the platform specified was not stored in the first place, the application prompt the user. +**** + +Examples: + +* `list` + +`removeplatform 3 facebook` + +Search with keyword "John" on facebook and display the search result in the browser window under the respective tabs. + + == FAQ *Q*: How do I transfer my data to another Computer? + @@ -246,7 +435,8 @@ _{explain how the user can enable/disable data encryption}_ == Command Summary -* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + +* *Login* : `login facebook` +* *Add* : `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` * *Clear* : `clear` * *Delete* : `delete INDEX` + @@ -255,10 +445,17 @@ e.g. `delete 3` e.g. `edit 2 n/James Lee e/jameslee@example.com` * *Find* : `find KEYWORD [MORE_KEYWORDS]` + e.g. `find James Jake` +* *Find with Tag* : `findtag KEYWORD [MORE_KEYWORDS]` + +e.g. `findtag friends neighbour` * *List* : `list` +* *Sort* : `sort` * *Help* : `help` * *Select* : `select INDEX` + e.g.`select 2` +* *Add Platform* : `addplatform INDEX [l/LINK]...` + +e.g. `addplatform 1 l/www.facebook.com/james.ho` +* *Remove Platform* : `removeplatform INDEX [smp/PLATFORM]...` + +e.g. `removeplatform 1 smp/facebook` * *History* : `history` * *Undo* : `undo` * *Redo* : `redo` diff --git a/docs/diagrams/SortCommandSequenceDiagram.pptx b/docs/diagrams/SortCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..166a82c21f33 Binary files /dev/null and b/docs/diagrams/SortCommandSequenceDiagram.pptx differ diff --git a/docs/images/AddPlatformCommandHighLevelSequenceDiagrams.png b/docs/images/AddPlatformCommandHighLevelSequenceDiagrams.png new file mode 100644 index 000000000000..851c8ba2bc06 Binary files /dev/null and b/docs/images/AddPlatformCommandHighLevelSequenceDiagrams.png differ diff --git a/docs/images/AddPlatformCommandSequenceDiagram.png b/docs/images/AddPlatformCommandSequenceDiagram.png new file mode 100644 index 000000000000..3ba78e432a92 Binary files /dev/null and b/docs/images/AddPlatformCommandSequenceDiagram.png differ diff --git a/docs/images/AddPlatformToPersonDiagram.png b/docs/images/AddPlatformToPersonDiagram.png new file mode 100644 index 000000000000..73dab94180d6 Binary files /dev/null and b/docs/images/AddPlatformToPersonDiagram.png differ diff --git a/docs/images/DifferentCaseSize.png b/docs/images/DifferentCaseSize.png new file mode 100644 index 000000000000..e25959ccc42a Binary files /dev/null and b/docs/images/DifferentCaseSize.png differ diff --git a/docs/images/FindMultipleTags.png b/docs/images/FindMultipleTags.png new file mode 100644 index 000000000000..83f160512828 Binary files /dev/null and b/docs/images/FindMultipleTags.png differ diff --git a/docs/images/FindTagInLogicClassDiagram.png b/docs/images/FindTagInLogicClassDiagram.png new file mode 100644 index 000000000000..034311451717 Binary files /dev/null and b/docs/images/FindTagInLogicClassDiagram.png differ diff --git a/docs/images/FindTagResultDiagram.png b/docs/images/FindTagResultDiagram.png new file mode 100644 index 000000000000..6cee08339834 Binary files /dev/null and b/docs/images/FindTagResultDiagram.png differ diff --git a/docs/images/FindTagSequenceDiagram.png b/docs/images/FindTagSequenceDiagram.png new file mode 100644 index 000000000000..4556c1a3e3e2 Binary files /dev/null and b/docs/images/FindTagSequenceDiagram.png differ diff --git a/docs/images/FindtagExecution.png b/docs/images/FindtagExecution.png new file mode 100644 index 000000000000..9faa9276a272 Binary files /dev/null and b/docs/images/FindtagExecution.png differ diff --git a/docs/images/InvalidCommandKeywordDiagram.png b/docs/images/InvalidCommandKeywordDiagram.png new file mode 100644 index 000000000000..0e91cda4a710 Binary files /dev/null and b/docs/images/InvalidCommandKeywordDiagram.png differ diff --git a/docs/images/InvalidPlatformCases.png b/docs/images/InvalidPlatformCases.png new file mode 100644 index 000000000000..e5758e57b215 Binary files /dev/null and b/docs/images/InvalidPlatformCases.png differ diff --git a/docs/images/SearchFunction.png b/docs/images/SearchFunction.png new file mode 100644 index 000000000000..7ae672594d1b Binary files /dev/null and b/docs/images/SearchFunction.png differ diff --git a/docs/images/SearchPersonEvent.png b/docs/images/SearchPersonEvent.png new file mode 100644 index 000000000000..35e24217562c Binary files /dev/null and b/docs/images/SearchPersonEvent.png differ diff --git a/docs/images/SocialMediaPlatformFactoryClassDiagram.png b/docs/images/SocialMediaPlatformFactoryClassDiagram.png new file mode 100644 index 000000000000..7fcd8bd644cb Binary files /dev/null and b/docs/images/SocialMediaPlatformFactoryClassDiagram.png differ diff --git a/docs/images/SocialMediaPlatformMapObjectDiagram.png b/docs/images/SocialMediaPlatformMapObjectDiagram.png new file mode 100644 index 000000000000..817ce4594c6c Binary files /dev/null and b/docs/images/SocialMediaPlatformMapObjectDiagram.png differ diff --git a/docs/images/SortCommandSequenceDiagram.png b/docs/images/SortCommandSequenceDiagram.png new file mode 100644 index 000000000000..88fb3f2374b5 Binary files /dev/null and b/docs/images/SortCommandSequenceDiagram.png differ diff --git a/docs/images/SortFilteredListDiagram.png b/docs/images/SortFilteredListDiagram.png new file mode 100644 index 000000000000..cfbe3b9129ab Binary files /dev/null and b/docs/images/SortFilteredListDiagram.png differ diff --git a/docs/images/SortSimilarNamesDiagram.png b/docs/images/SortSimilarNamesDiagram.png new file mode 100644 index 000000000000..3c5db5754fe0 Binary files /dev/null and b/docs/images/SortSimilarNamesDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..fa581bfb9878 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/kevinchuangch.png b/docs/images/kevinchuangch.png new file mode 100644 index 000000000000..b0a96521cf74 Binary files /dev/null and b/docs/images/kevinchuangch.png differ diff --git a/docs/images/nethergale.png b/docs/images/nethergale.png new file mode 100644 index 000000000000..01a165daea83 Binary files /dev/null and b/docs/images/nethergale.png differ diff --git a/docs/images/shadow2496.png b/docs/images/shadow2496.png new file mode 100644 index 000000000000..1215cb8de5da Binary files /dev/null and b/docs/images/shadow2496.png differ diff --git a/docs/team/chuangchengheng.adoc b/docs/team/chuangchengheng.adoc new file mode 100644 index 000000000000..020e480b24aa --- /dev/null +++ b/docs/team/chuangchengheng.adoc @@ -0,0 +1,58 @@ += Chuang Cheng Heng - Project Portfolio +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Media Socializer + +--- + +== Overview + +Media Socializer is a one-stop social media desktop application made to help users to keep track of the many social media platforms they are currently using. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: added *the ability to search for person on social media platforms* +** What it does: allows the user to perform the search function for person's profile on the social media platforms through the application with a single command. +** Justification: This feature grants the user the ability to search for other person's profile on either the stated social media platforms or all the platforms available through the application. Thus, the user no longer need to go on to the different social media platform and perform the search repeatedly. This feature brings the application one step closer to the goal of being an one-stop social media application. +** Highlights: This feature has to be able to display the search results of different social media platforms in a convenient way, as such, it requires the implementor to come up with a UI allows the user to jump between search results easily. The implementation is +** Credits: The design of the UI is inspired by the tab interface of the internet browsers (i.e. Firefox, Google Chrome, and etc) + +* *Minor enhancement*: added a findtag command that allows the user to find persons whose tags contain the input keywords. + +* *Code contributed*: [https://github.com/CS2103JAN2018-F12-B3/main/blob/master/collated/functional/KevinChuangCH.md[Functional code]] [https://github.com/CS2103JAN2018-F12-B3/main/blob/master/collated/test/KevinChuangCH.md[Test code]] + +* *Other contributions*: + +** Project management: +*** Ensure the closure of milestone `v1.3` - `v1.5` (4 milestones including 1.5rc) on GitHub +** Enhancements to existing features: +*** Modified the WebBrowser UI to suit the requirement of Media Socializer (Pull request https://github.com/CS2103JAN2018-F12-B3/main/pull/90[#90]) +*** Edited the typical persons to support testing of findtag feature (Commit https://github.com/CS2103JAN2018-F12-B3/main/pull/57/commits/81e4756a3c4b96af8ef241bb2f09b13da5cdc370[1], https://github.com/CS2103JAN2018-F12-B3/main/pull/57/commits/a96b25048d02a8c135a9e59cd3001efa591f64fc[2], https://github.com/CS2103JAN2018-F12-B3/main/pull/57/commits/8c9c32c0f41d9f15e1f8c440491c3eafd35383a0[3]) +** Documentation: +*** Update About Us: https://github.com/CS2103JAN2018-F12-B3/main/pull/56[#56] +** Community: +*** Follow up with reported bugs (example: https://github.com/CS2103JAN2018-F12-B3/main/issues/112[1], https://github.com/CS2103JAN2018-F12-B3/main/issues/109[2]) + + + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=findtag] + +include::../UserGuide.adoc[tag=search] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=findtag] + +include::../DeveloperGuide.adoc[tag=search] diff --git a/docs/team/seunghwanchoi.adoc b/docs/team/seunghwanchoi.adoc new file mode 100644 index 000000000000..0dfa757e454b --- /dev/null +++ b/docs/team/seunghwanchoi.adoc @@ -0,0 +1,71 @@ += John Doe - Project Portfolio +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: AddressBook - Level 4 + +--- + +== Overview + +AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: added *the ability to undo/redo previous commands* +** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. +** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. +** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. +** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ + +* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. + +* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub +** Enhancements to existing features: +*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) +*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) +** Documentation: +*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] +*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) +*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) +** Tools: +*** Integrated a third party library (Natty) to the project (https://github.com[#42]) +*** Integrated a new Github plugin (CircleCI) to the team repo + +_{you can add/remove categories in the list above}_ + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=undoredo] + +include::../UserGuide.adoc[tag=dataencryption] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=undoredo] + +include::../DeveloperGuide.adoc[tag=dataencryption] + + +== PROJECT: PowerPointLabs + +--- + +_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/tanweijie.adoc b/docs/team/tanweijie.adoc new file mode 100644 index 000000000000..7ac8e64cdbec --- /dev/null +++ b/docs/team/tanweijie.adoc @@ -0,0 +1,65 @@ += Tan Wei Jie - Project Portfolio +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Media Socializer + +--- + +== Overview + +Media Socializer is a one-stop social media desktop application used to help social media users keep track of all the social media platforms their contacts are using. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: Added *the ability to manage the social media platforms of each person* +** What it does: Allows the user to add the associated social media platforms to the specified person by typing in the links to the relevant profile pages or remove them by entering the names of the platforms. +** Justification: This feature improves the product significantly because a user can have the application keep track of all the social media profiles of their contacts. +** Highlights: This enhancement affects the overall feel of the product, since it is a core feature of the application. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required touching upon multiple components to properly realise such a feature. + +* *Minor enhancement*: Added a sort command that allows the user to alphabetically sort the people in the list. + +* *Code contributed*: [https://github.com/CS2103JAN2018-F12-B3/main/blob/master/collated/functional/Nethergale.md[Functional code]] [https://github.com/CS2103JAN2018-F12-B3/main/blob/master/collated/test/Nethergale.md[Test code]] + +* *Other contributions*: Assisted team members in fulfilling certain requirements for the milestones (Pull requests https://github.com/CS2103JAN2018-F12-B3/main/pull/70[#70], https://github.com/CS2103JAN2018-F12-B3/main/pull/99[#99]) + +** Project management: +*** Managed releases `v1.5rc` - `v1.5` (2 releases) on GitHub +*** Identified the deliverables for each release to keep track of +** Enhancements to existing features: +*** Updated the GUI to show social media icons for each person (Pull request https://github.com/CS2103JAN2018-F12-B3/main/pull/85[#85]) +*** Wrote tests for existing features to maintain coverage above 90% (Pull requests https://github.com/CS2103JAN2018-F12-B3/main/pull/87[#87], https://github.com/CS2103JAN2018-F12-B3/main/pull/140[#140]) +** Documentation: +*** Updated appendix of the Developer Guide to match the goals of the application: https://github.com/CS2103JAN2018-F12-B3/main/pull/45[#45] +*** Added tags to the User Guide and Developer Guide to link portfolios to their associated sections: https://github.com/CS2103JAN2018-F12-B3/main/pull/69[#69] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2103JAN2018-F12-B3/main/pull/56[#56], https://github.com/CS2103JAN2018-F12-B3/main/pull/32[#32] +*** Bug reports followed up with testers (examples: https://github.com/CS2103JAN2018-F12-B3/main/issues/108[1], https://github.com/CS2103JAN2018-F12-B3/main/issues/110[2]) +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103JAN2018-W15-B3/main/issues/128[1], https://github.com/CS2103JAN2018-W15-B3/main/issues/123[2]) + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=sort] + +include::../UserGuide.adoc[tag=addplatform] + +include::../UserGuide.adoc[tag=removeplatform] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=product-scope] + +include::../DeveloperGuide.adoc[tag=user-stories] + +include::../DeveloperGuide.adoc[tag=sort] + +include::../DeveloperGuide.adoc[tag=addplatform] diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index fa0800d55cb9..c8c0f2e7c7d6 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -40,7 +40,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 5, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -54,7 +54,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing Media Socializer ]==========================="); super.init(); config = initConfig(getApplicationParameter("config")); @@ -183,13 +183,13 @@ private void initEventsCenter() { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting Media Socializer " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Media Socializer ] ============================="); ui.stop(); try { storage.saveUserPrefs(userPrefs); diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index 8f4d737d0e24..550bd45f808d 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/address/commons/core/Config.java @@ -11,7 +11,7 @@ public class Config { public static final String DEFAULT_CONFIG_FILE = "config.json"; // Config values customizable through config file - private String appTitle = "Address App"; + private String appTitle = "Media Socializer"; private Level logLevel = Level.INFO; private String userPrefsFilePath = "preferences.json"; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..53496075151c 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -9,5 +9,6 @@ public class Messages { public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_ADDRESS_BOOK_EMPTY = "Address book is empty."; } diff --git a/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java index a6d5274a97e0..59270245abf0 100644 --- a/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java +++ b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java @@ -7,9 +7,17 @@ */ public class NewResultAvailableEvent extends BaseEvent { + //@@author shadow2496 + public final boolean hasError; + + //@@author public final String message; - public NewResultAvailableEvent(String message) { + public NewResultAvailableEvent(String message, boolean hasError) { + //@@author shadow2496 + this.hasError = hasError; + + //@@author this.message = message; } diff --git a/src/main/java/seedu/address/commons/events/ui/SearchPersonEvent.java b/src/main/java/seedu/address/commons/events/ui/SearchPersonEvent.java new file mode 100644 index 000000000000..c1c07d528420 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/SearchPersonEvent.java @@ -0,0 +1,32 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +//@@author KevinChuangCH +/** + * Represents a search for person. + */ +public class SearchPersonEvent extends BaseEvent { + + private final String searchName; + private final String platformToSearch; + + public SearchPersonEvent(String platformToSearch, String searchName) { + this.searchName = searchName; + this.platformToSearch = platformToSearch; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + public String getSearchName() { + return searchName; + } + + public String getPlatform() { + return platformToSearch; + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/ShowLoginDialogRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/ShowLoginDialogRequestEvent.java new file mode 100644 index 000000000000..f7864fb05e38 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ShowLoginDialogRequestEvent.java @@ -0,0 +1,21 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +//@@author shadow2496 +/** + * Indicates a request to view the login dialog. + */ +public class ShowLoginDialogRequestEvent extends BaseEvent { + + public final String loadUrl; + + public ShowLoginDialogRequestEvent(String loadUrl) { + this.loadUrl = loadUrl; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..0029c803d961 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -19,6 +19,12 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; + //@@author shadow2496 + /** Passes the verification code for an access token */ + void passVerificationCode(String code); + + //@@author + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9f6846bdfc74..086a8f8ea9ce 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -45,6 +45,14 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } } + //@@author shadow2496 + @Override + public void passVerificationCode(String code) { + model.setVerificationCode(code); + } + + //@@author + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index c334710c0ea3..22872999369b 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -18,6 +18,8 @@ public class AddCommand extends UndoableCommand { public static final String COMMAND_WORD = "add"; + public static final String COMMAND_ALIAS = "a"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + "Parameters: " + PREFIX_NAME + "NAME " @@ -31,7 +33,15 @@ public class AddCommand extends UndoableCommand { + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_TAG + "owesMoney" + "\n" + + "or\n" + + "Example: " + COMMAND_ALIAS + " " + + PREFIX_NAME + "Lee Hua " + + PREFIX_PHONE + "81227675 " + + PREFIX_EMAIL + "leehua@example.com " + + PREFIX_ADDRESS + "318, Simei Street 2, #05-05 " + + PREFIX_TAG + "friends " + + PREFIX_TAG + "lovesCat"; 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"; diff --git a/src/main/java/seedu/address/logic/commands/AddPlatformCommand.java b/src/main/java/seedu/address/logic/commands/AddPlatformCommand.java new file mode 100644 index 000000000000..492ec9216019 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddPlatformCommand.java @@ -0,0 +1,141 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.smplatform.SocialMediaPlatformFactory; + +//@@author Nethergale +/** + * Adds social media platforms to the specified person in the address book. + */ +public class AddPlatformCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "addplatform"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds social media platforms to the person " + + "identified by the index number used in the last person listing through website links.\n" + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_LINK + "LINK]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_LINK + "www.facebook.com/johndoe"; + + public static final String MESSAGE_ADD_PLATFORM_SUCCESS = "Platform(s) successfully added to %1$s."; + public static final String MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS = "Platform(s) successfully cleared for %1$s."; + public static final String MESSAGE_LINK_COLLECTION_EMPTY = "At least 1 link field should be specified."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + private final Index index; + private final Map linkMap; + private final Map socialMediaPlatformMap; + + private Person personToEdit; + private Person editedPerson; + + /** + * @param index of the person in the filtered person list to edit + * @param linkMap as defined by the social media platform type as key and link as value + */ + public AddPlatformCommand(Index index, Map linkMap) { + requireNonNull(index); + requireNonNull(linkMap); + + this.index = index; + this.linkMap = linkMap; + socialMediaPlatformMap = new HashMap<>(); + } + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + requireNonNull(personToEdit); + requireNonNull(editedPerson); + + 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); + + if (socialMediaPlatformMap.isEmpty()) { + return new CommandResult(String.format(MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS, editedPerson.getName())); + } + return new CommandResult(String.format(MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + try { + addToSocialMediaPlatformMap(); + } catch (IllegalValueException ive) { + throw new CommandException(ive.getMessage()); + } + + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(index.getZeroBased()); + addCurrentSocialMediaPlatforms(); + editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), socialMediaPlatformMap, personToEdit.getTags()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddPlatformCommand)) { + return false; + } + + // state check + AddPlatformCommand e = (AddPlatformCommand) other; + return index.equals(e.index) + && linkMap.equals(e.linkMap); + } + + /** + * Constructs social media platform objects depending on the link type and adds them to the map. + */ + private void addToSocialMediaPlatformMap() throws IllegalValueException { + for (String type : linkMap.keySet()) { + socialMediaPlatformMap.put(type, + SocialMediaPlatformFactory.getSocialMediaPlatform(type, linkMap.get(type))); + } + } + + /** + * Adds back the social media platforms not found in the edited person into the map only if it is not empty. + */ + private void addCurrentSocialMediaPlatforms() { + if (!socialMediaPlatformMap.isEmpty()) { + for (String key : personToEdit.getSocialMediaPlatformMap().keySet()) { + socialMediaPlatformMap.putIfAbsent(key, personToEdit.getSocialMediaPlatformMap().get(key)); + } + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index e6c3a3e034bc..9ffa06b306d8 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -26,6 +27,7 @@ import seedu.address.model.person.Phone; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; /** @@ -106,9 +108,11 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Map updatedSocialMediaPlatformMap = personToEdit.getSocialMediaPlatformMap(); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, + updatedAddress, updatedSocialMediaPlatformMap, updatedTags); } @Override diff --git a/src/main/java/seedu/address/logic/commands/FindWithTagCommand.java b/src/main/java/seedu/address/logic/commands/FindWithTagCommand.java new file mode 100644 index 000000000000..158404922c0e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindWithTagCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import seedu.address.model.person.TagContainsKeywordsPredicate; + +//@@author KevinChuangCH +/** + * Finds and lists all persons in address book whose tags contain any of the argument keywords. + * Keyword matching is case sensitive. + */ +public class FindWithTagCommand extends Command { + + public static final String COMMAND_WORD = "findtag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose tags 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 + " friends owesMoney"; + + private final TagContainsKeywordsPredicate predicate; + + public FindWithTagCommand(TagContainsKeywordsPredicate 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 FindWithTagCommand // instanceof handles nulls + && this.predicate.equals(((FindWithTagCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/LoginCommand.java b/src/main/java/seedu/address/logic/commands/LoginCommand.java new file mode 100644 index 000000000000..c8fe498bf650 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoginCommand.java @@ -0,0 +1,48 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PASSWORD; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; + +import seedu.address.model.account.Account; + +//@@author shadow2496 +/** + * Logs into a social media platform using the user's account information. + */ +public class LoginCommand extends Command { + + public static final String COMMAND_WORD = "login"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Logs into a social media platform using " + + "the user's name and password. " + + "Parameters: " + + PREFIX_USERNAME + "USERNAME " + + PREFIX_PASSWORD + "PASSWORD\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_USERNAME + "johndoe " + + PREFIX_PASSWORD + "jd9876"; + + public static final String MESSAGE_SUCCESS = "Logged in account: %1$s"; + + private final Account accountToLogin; + + public LoginCommand(Account account) { + requireNonNull(account); + accountToLogin = account; + } + + @Override + public CommandResult execute() { + requireNonNull(model); + model.loginAccount(accountToLogin); + return new CommandResult(String.format(MESSAGE_SUCCESS, accountToLogin)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof LoginCommand // instanceof handles nulls + && this.accountToLogin.equals(((LoginCommand) other).accountToLogin)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemovePlatformCommand.java b/src/main/java/seedu/address/logic/commands/RemovePlatformCommand.java new file mode 100644 index 000000000000..8f9b5e9d5804 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemovePlatformCommand.java @@ -0,0 +1,111 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_SOCIAL_MEDIA_PLATFORM; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.smplatform.SocialMediaPlatform; + +//@@author Nethergale +/** + * Removes the specified social media platforms of a person identified + * using it's last displayed index from the address book. + */ +public class RemovePlatformCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "removeplatform"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes specified social media platforms " + + "of the person identified by the index number used in the last person listing.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_SOCIAL_MEDIA_PLATFORM + "PLATFORM]...\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_SOCIAL_MEDIA_PLATFORM + "facebook"; + + public static final String MESSAGE_REMOVE_PLATFORM_SUCCESS = "Platform(s) removed from %1$s."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_PLATFORM_MAP_NOT_EDITED = "No changes can be made from the given platform info."; + + private final Index targetIndex; + private final Set platformsToRemove; + private final Map socialMediaPlatformMap; + + private Person personToEdit; + private Person editedPerson; + + public RemovePlatformCommand(Index targetIndex, Set platformsToRemove) { + this.targetIndex = targetIndex; + this.platformsToRemove = platformsToRemove; + socialMediaPlatformMap = new HashMap<>(); + } + + + @Override + public CommandResult executeUndoableCommand() throws CommandException { + 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"); + } + + return new CommandResult(String.format(MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName())); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + personToEdit = lastShownList.get(targetIndex.getZeroBased()); + + try { + removeFromSocialMediaPlatformMap(); + } catch (IllegalValueException ive) { + throw new CommandException(ive.getMessage()); + } + + editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), socialMediaPlatformMap, personToEdit.getTags()); + } + + /** + * Removes the social media platform mappings from the map if specified. + * + * @throws IllegalValueException if no changes are made + */ + private void removeFromSocialMediaPlatformMap() throws IllegalValueException { + if (!platformsToRemove.isEmpty()) { + socialMediaPlatformMap.putAll(personToEdit.getSocialMediaPlatformMap()); + + for (String platform : platformsToRemove) { + socialMediaPlatformMap.remove(platform.toLowerCase()); + } + + if (socialMediaPlatformMap.equals(personToEdit.getSocialMediaPlatformMap())) { + throw new IllegalValueException(MESSAGE_PLATFORM_MAP_NOT_EDITED); + } + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RemovePlatformCommand // instanceof handles nulls + && this.targetIndex.equals(((RemovePlatformCommand) other).targetIndex) // state check + && this.platformsToRemove.equals(((RemovePlatformCommand) other).platformsToRemove)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SearchCommand.java b/src/main/java/seedu/address/logic/commands/SearchCommand.java new file mode 100644 index 000000000000..8e2ed0ea69df --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SearchCommand.java @@ -0,0 +1,52 @@ +package seedu.address.logic.commands; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.SearchPersonEvent; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.smplatform.Facebook; +import seedu.address.model.smplatform.Twitter; + +//@@author KevinChuangCH +/** + * Search for a person with the input name either on all available social media platforms + * or the stated social media platform if it is available.. + */ +public class SearchCommand extends Command { + + public static final String COMMAND_WORD = "search"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Search for a person with the input name on the input social media platform if it is available.\n" + + "If no platform is stated, then the search will be performed on all available social media platforms.\n" + + "User can use alias to indicate the social media platform.\n" + + "Parameters: [PLATFORM], NAME (PLATFORM is case sensitive and NAME should not be blank)\n" + + "Example: " + COMMAND_WORD + " " + Facebook.PLATFORM_KEYWORD + ", Tom\n" + + "or\n" + + "Example: " + COMMAND_WORD + " " + Twitter.PLATFORM_ALIAS + ", Sam\n" + + "or\n" + + "Example: " + COMMAND_WORD + " Jason\n"; + + public static final String MESSAGE_SEARCH_PERSON_SUCCESS = "Searched Person: %1$s"; + + private final String targetPlatform; + private final String targetName; + + public SearchCommand(String targetPlatform, String targetName) { + this.targetName = targetName; + this.targetPlatform = targetPlatform; + } + + @Override + public CommandResult execute() throws CommandException { + + EventsCenter.getInstance().post(new SearchPersonEvent(targetPlatform, targetName)); + return new CommandResult(String.format(MESSAGE_SEARCH_PERSON_SUCCESS, targetName)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SearchCommand // instanceof handles nulls + && this.targetName.equals(((SearchCommand) other).targetName)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 000000000000..aa7f6116f3ed --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,54 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import com.google.common.collect.Ordering; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Person; + +//@@author Nethergale +/** + * Displays the current list of persons in the address book to the user sorted alphabetically. + */ +public class SortCommand extends UndoableCommand { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays the current list of persons in the " + + "address book sorted in alphabetical order with index numbers.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Address book sorted successfully!"; + public static final String MESSAGE_FAILURE = "Address book has already been sorted."; + + private List personList; + + /** + * Returns true if person list is sorted. + */ + private boolean isListSorted() { + return Ordering.from(Person.nameComparator()).isOrdered(personList); + } + + @Override + public CommandResult executeUndoableCommand() { + requireNonNull(model); + model.sortAllPersons(); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + protected void preprocessUndoableCommand() throws CommandException { + personList = model.getAddressBook().getPersonList(); + if (personList.isEmpty()) { + throw new CommandException(Messages.MESSAGE_ADDRESS_BOOK_EMPTY); + } + if (isListSorted()) { + throw new CommandException(MESSAGE_FAILURE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3c729b388554..64009d450a7f 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -7,6 +7,8 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -18,6 +20,7 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; /** @@ -44,9 +47,10 @@ public AddCommand parse(String args) throws ParseException { Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE)).get(); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL)).get(); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS)).get(); + Map socialMediaPlatformMap = new HashMap<>(); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, socialMediaPlatformMap, tagList); return new AddCommand(person); } catch (IllegalValueException ive) { diff --git a/src/main/java/seedu/address/logic/parser/AddPlatformCommandParser.java b/src/main/java/seedu/address/logic/parser/AddPlatformCommandParser.java new file mode 100644 index 000000000000..9496f61fc75e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddPlatformCommandParser.java @@ -0,0 +1,67 @@ +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_LINK; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.AddPlatformCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.smplatform.Link; + +//@@author Nethergale +/** + * Parses input arguments and creates a new AddPlatformCommand object + */ +public class AddPlatformCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddPlatformCommand + * and returns an AddPlatformCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public AddPlatformCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_LINK); + + Index index; + Map linkMap; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPlatformCommand.MESSAGE_USAGE)); + } + + try { + linkMap = parseLinksForAddPlatform(argMultimap.getAllValues(PREFIX_LINK)); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + + return new AddPlatformCommand(index, linkMap); + } + + /** + * Parses {@code Collection links} into a {@code Map} if {@code links} is non-empty. + * If {@code links} contain only one element which is an empty string, it will be parsed into a + * {@code Map} containing zero links. + * + * @throws IllegalValueException if user does not specify even a single link prefix. + */ + private Map parseLinksForAddPlatform(Collection links) throws IllegalValueException { + assert links != null; + + if (links.isEmpty()) { + throw new IllegalValueException(AddPlatformCommand.MESSAGE_LINK_COLLECTION_EMPTY); + } + Collection linkSet = links.size() == 1 && links.contains("") ? Collections.emptySet() : links; + return ParserUtil.parseLinks(linkSet); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b7d57f5db86a..21e40cae0c51 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -7,17 +7,23 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddPlatformCommand; 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.FindWithTagCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoginCommand; import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.RemovePlatformCommand; +import seedu.address.logic.commands.SearchCommand; import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.SortCommand; import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -48,12 +54,24 @@ public Command parseCommand(String userInput) throws ParseException { final String arguments = matcher.group("arguments"); switch (commandWord) { + //@@author shadow2496 + case LoginCommand.COMMAND_WORD: + return new LoginCommandParser().parse(arguments); + + //@@author + case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); + case AddCommand.COMMAND_ALIAS: + return new AddCommandParser().parse(arguments); + case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); + case SearchCommand.COMMAND_WORD: + return new SearchCommandParser().parse(arguments); + case SelectCommand.COMMAND_WORD: return new SelectCommandParser().parse(arguments); @@ -66,6 +84,9 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FindWithTagCommand.COMMAND_WORD: + return new FindWithTagCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); @@ -84,6 +105,17 @@ public Command parseCommand(String userInput) throws ParseException { case RedoCommand.COMMAND_WORD: return new RedoCommand(); + //@@author Nethergale + case SortCommand.COMMAND_WORD: + return new SortCommand(); + + case AddPlatformCommand.COMMAND_WORD: + return new AddPlatformCommandParser().parse(arguments); + + case RemovePlatformCommand.COMMAND_WORD: + return new RemovePlatformCommandParser().parse(arguments); + + //@@author default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..00b10ba8c6c1 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,13 @@ public class CliSyntax { /* Prefix definitions */ + public static final Prefix PREFIX_USERNAME = new Prefix("id/"); + public static final Prefix PREFIX_PASSWORD = new Prefix("pw/"); 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_LINK = new Prefix("l/"); + public static final Prefix PREFIX_SOCIAL_MEDIA_PLATFORM = new Prefix("smp/"); } diff --git a/src/main/java/seedu/address/logic/parser/FindWithTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindWithTagCommandParser.java new file mode 100644 index 000000000000..35924d2d2efe --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindWithTagCommandParser.java @@ -0,0 +1,34 @@ +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.FindWithTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagContainsKeywordsPredicate; + +//@@author KevinChuangCH +/** + * Parses input arguments and creates a new FindWithTagCommand object + */ +public class FindWithTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindWithTagCommand + * and returns an FindWithTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindWithTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindWithTagCommand.MESSAGE_USAGE)); + } + + String[] tagKeywords = trimmedArgs.split("\\s+"); + + return new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList(tagKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/LoginCommandParser.java b/src/main/java/seedu/address/logic/parser/LoginCommandParser.java new file mode 100644 index 000000000000..a9202f46c7fa --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LoginCommandParser.java @@ -0,0 +1,54 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PASSWORD; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; + +import java.util.stream.Stream; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.account.Account; +import seedu.address.model.account.Password; +import seedu.address.model.account.Username; + +//@@author shadow2496 +/** + * Parses input arguments and creates a new LoginCommand object + */ +public class LoginCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoginCommand + * and returns an LoginCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LoginCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_USERNAME, PREFIX_PASSWORD); + + if (!arePrefixesPresent(argMultimap, PREFIX_USERNAME, PREFIX_PASSWORD) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + + try { + Username username = ParserUtil.parseUsername(argMultimap.getValue(PREFIX_USERNAME)).get(); + Password password = ParserUtil.parsePassword(argMultimap.getValue(PREFIX_PASSWORD)).get(); + + Account account = new Account(username, password); + + return new LoginCommand(account); + } catch (IllegalValueException ive) { + throw new ParseException(ive.getMessage(), ive); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 5d6d4ae3f7b1..0e4a585eb335 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -3,17 +3,23 @@ import static java.util.Objects.requireNonNull; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; 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.account.Password; +import seedu.address.model.account.Username; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; /** @@ -30,6 +36,57 @@ 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."; + //@@author shadow2496 + /** + * Parses a {@code String username} into an {@code Username}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code username} is invalid. + */ + public static Username parseUsername(String username) throws IllegalValueException { + requireNonNull(username); + String trimmedUsername = username.trim(); + if (!Username.isValidUsername(trimmedUsername)) { + throw new IllegalValueException(Username.MESSAGE_USERNAME_CONSTRAINTS); + } + return new Username(trimmedUsername); + } + + /** + * Parses a {@code Optional username} into an {@code Optional} if {@code username} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseUsername(Optional username) throws IllegalValueException { + requireNonNull(username); + return username.isPresent() ? Optional.of(parseUsername(username.get())) : Optional.empty(); + } + + /** + * Parses a {@code String password} into an {@code Password}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code password} is invalid. + */ + public static Password parsePassword(String password) throws IllegalValueException { + requireNonNull(password); + String trimmedPassword = password.trim(); + if (!Password.isValidPassword(trimmedPassword)) { + throw new IllegalValueException(Password.MESSAGE_PASSWORD_CONSTRAINTS); + } + return new Password(trimmedPassword); + } + + /** + * Parses a {@code Optional password} into an {@code Optional} if {@code password} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parsePassword(Optional password) throws IllegalValueException { + requireNonNull(password); + return password.isPresent() ? Optional.of(parsePassword(password.get())) : Optional.empty(); + } + + //@@author + /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. @@ -43,6 +100,36 @@ public static Index parseIndex(String oneBasedIndex) throws IllegalValueExceptio return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + //@@author KevinChuangCH + /** + * Checks for the validation of {@code inputPlatform} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified name is invalid (not following the name regex). + */ + public static String parsePlatformToSearch(String inputPlatform) throws IllegalValueException { + requireNonNull(inputPlatform); + String trimmedInputPlatform = inputPlatform.trim(); + if (!SocialMediaPlatform.isValidPlatform(trimmedInputPlatform)) { + throw new IllegalValueException(SocialMediaPlatform.MESSAGE_PLATFORM_CONSTRAINTS); + } + return trimmedInputPlatform; + } + + /** + * Checks for the validation of {@code inputName} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws IllegalValueException if the specified name is invalid (not following the name regex). + */ + public static String parseSearchName(String inputName) throws IllegalValueException { + requireNonNull(inputName); + String trimmedInputName = inputName.trim(); + if (!trimmedInputName.matches(SocialMediaPlatform.USERNAME_VALIDATION_REGEX)) { + throw new IllegalValueException(SocialMediaPlatform.MESSAGE_USERNAME_CONSTRAINTS); + } + return trimmedInputName; + } + //@@author + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -139,6 +226,43 @@ public static Optional parseEmail(Optional email) throws IllegalV return email.isPresent() ? Optional.of(parseEmail(email.get())) : Optional.empty(); } + //@@author Nethergale + /** + * Parses a {@code String link} into a {@code Link}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code link} is invalid. + */ + public static Link parseLink(String link) throws IllegalValueException { + requireNonNull(link); + String trimmedLink = link.trim(); + if (!Link.isValidLink(trimmedLink)) { + throw new IllegalValueException(Link.MESSAGE_INVALID_LINK); + } + return new Link(trimmedLink); + } + + /** + * Parses {@code Collection links} into a {@code Map}. + * + * @throws IllegalValueException if any social media platform is going to have more than one link each. + */ + public static Map parseLinks(Collection links) throws IllegalValueException { + requireNonNull(links); + final Map linkMap = new HashMap<>(); + for (String linkStr : links) { + String linkType = Link.getLinkType(linkStr); + Link link = parseLink(linkStr); + if ((linkType.equals(Link.FACEBOOK_LINK_TYPE) || linkType.equals(Link.TWITTER_LINK_TYPE)) + && linkMap.containsKey(linkType)) { + throw new IllegalValueException(Link.MESSAGE_LINK_CONSTRAINTS); + } + linkMap.put(linkType, link); + } + return linkMap; + } + + //@@author /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/logic/parser/RemovePlatformCommandParser.java b/src/main/java/seedu/address/logic/parser/RemovePlatformCommandParser.java new file mode 100644 index 000000000000..b7910ef7e8f2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemovePlatformCommandParser.java @@ -0,0 +1,44 @@ +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_SOCIAL_MEDIA_PLATFORM; + +import java.util.HashSet; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.RemovePlatformCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author Nethergale +/** + * Parses input arguments and creates a new RemovePlatformCommand object + */ +public class RemovePlatformCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemovePlatformCommand + * and returns a RemovePlatformCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemovePlatformCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SOCIAL_MEDIA_PLATFORM); + + Index index; + Set platformSet = new HashSet<>(); + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + platformSet.addAll(argMultimap.getAllValues(PREFIX_SOCIAL_MEDIA_PLATFORM)); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemovePlatformCommand.MESSAGE_USAGE)); + } + + return new RemovePlatformCommand(index, platformSet); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SearchCommandParser.java b/src/main/java/seedu/address/logic/parser/SearchCommandParser.java new file mode 100644 index 000000000000..c72f3790be75 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SearchCommandParser.java @@ -0,0 +1,45 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.SearchCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author KevinChuangCH +/** + * Parses input arguments and creates a new SearchCommand object + */ +public class SearchCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SearchCommand + * and returns an SearchCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SearchCommand parse(String args) throws ParseException { + String[] splitArgs = args.split(", "); + if (splitArgs.length == 1) { + String platform = "all"; + try { + String inputName = ParserUtil.parseSearchName(splitArgs[0]); + return new SearchCommand(platform, inputName); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } else if (splitArgs.length == 2) { + try { + String platform = ParserUtil.parsePlatformToSearch(splitArgs[0]); + String inputName = ParserUtil.parseSearchName(splitArgs[1]); + return new SearchCommand(platform, inputName); + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index f8d0260de159..ba2311305052 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -11,6 +11,9 @@ import java.util.stream.Collectors; import javafx.collections.ObservableList; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.ShowLoginDialogRequestEvent; +import seedu.address.model.account.Account; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; import seedu.address.model.person.exceptions.DuplicatePersonException; @@ -24,6 +27,7 @@ */ public class AddressBook implements ReadOnlyAddressBook { + private Account account; private final UniquePersonList persons; private final UniqueTagList tags; @@ -76,6 +80,24 @@ public void resetData(ReadOnlyAddressBook newData) { } } + //@@author shadow2496 + //// account-level operations + + /** + * Logs into a social media platform using {@code account}. + */ + public void loginAccount(Account account) { + this.account = account; + String loadUrl = this.account.getLoginDialogUrl(); + EventsCenter.getInstance().post(new ShowLoginDialogRequestEvent(loadUrl)); + } + + public void setVerificationCode(String code) { + account.setCode(code); + } + + //@@author + //// person-level operations /** @@ -131,8 +153,8 @@ private Person syncWithMasterTagList(Person person) { // 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))); - return new Person( - person.getName(), person.getPhone(), person.getEmail(), person.getAddress(), correctTagReferences); + return new Person(person.getName(), person.getPhone(), person.getEmail(), + person.getAddress(), person.getSocialMediaPlatformMap(), correctTagReferences); } /** @@ -147,6 +169,15 @@ public boolean removePerson(Person key) throws PersonNotFoundException { } } + //@@author Nethergale + /** + * Sorts all persons by name in alphabetical order in the address book. + */ + public void sort() { + persons.sort(); + } + + //@@author //// tag-level operations public void addTag(Tag t) throws UniqueTagList.DuplicateTagException { diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index 4a6079ce0199..cec737b586a9 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,6 +3,7 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; +import seedu.address.model.account.Account; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -20,6 +21,15 @@ public interface Model { /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + //@@author shadow2496 + /** Logs in with the given account */ + void loginAccount(Account account); + + /** Sets the verification code in an account. */ + void setVerificationCode(String code); + + //@@author + /** Deletes the given person. */ void deletePerson(Person target) throws PersonNotFoundException; @@ -36,6 +46,11 @@ public interface Model { void updatePerson(Person target, Person editedPerson) throws DuplicatePersonException, PersonNotFoundException; + /** + * Sorts all persons alphabetically. + */ + void sortAllPersons(); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 22a7d0eb3f4d..05b81771b12a 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -12,6 +12,7 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.model.account.Account; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -59,6 +60,20 @@ private void indicateAddressBookChanged() { raise(new AddressBookChangedEvent(addressBook)); } + //@@author shadow2496 + @Override + public void loginAccount(Account account) { + addressBook.loginAccount(account); + //indicateAddressBookChanged(); + } + + @Override + public void setVerificationCode(String code) { + addressBook.setVerificationCode(code); + } + + //@@author + @Override public synchronized void deletePerson(Person target) throws PersonNotFoundException { addressBook.removePerson(target); @@ -81,6 +96,14 @@ public void updatePerson(Person target, Person editedPerson) indicateAddressBookChanged(); } + //@@author Nethergale + @Override + public void sortAllPersons() { + addressBook.sort(); + indicateAddressBookChanged(); + } + + //@@author //=========== Filtered Person List Accessors ============================================================= /** diff --git a/src/main/java/seedu/address/model/account/Account.java b/src/main/java/seedu/address/model/account/Account.java new file mode 100644 index 000000000000..be09858238ba --- /dev/null +++ b/src/main/java/seedu/address/model/account/Account.java @@ -0,0 +1,100 @@ +package seedu.address.model.account; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import com.restfb.DefaultFacebookClient; +import com.restfb.FacebookClient; +import com.restfb.Version; +import com.restfb.scope.ScopeBuilder; + +//@@author shadow2496 +/** + * Represents an Account in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Account { + + private static final String APP_ID = "366902457122089"; + private static final String APP_SECRET = "9cdbed37c0780e75e995381f0688a8d7"; + private static final String REDIRECT_URL = "https://www.facebook.com/connect/login_success.html"; + + private final Username username; + private final Password password; + + private ScopeBuilder scopeBuilder; + private FacebookClient client; + private String code; + + /** + * Every field must be present and not null. + */ + public Account(Username username, Password password) { + requireAllNonNull(username, password); + this.username = username; + this.password = password; + + setScopeBuilder(); + setClient(); + } + + public Username getUsername() { + return username; + } + + public Password getPassword() { + return password; + } + + /** + * Sets the scope builder which is a set of permissions. + */ + private void setScopeBuilder() { + scopeBuilder = new ScopeBuilder(); + } + + /** + * Sets the client used to get an access token. + */ + private void setClient() { + client = new DefaultFacebookClient(Version.VERSION_2_12); + } + + public void setCode(String code) { + this.code = code; + } + + /** + * Returns the login dialog url using {@link #client}. + */ + public String getLoginDialogUrl() { + return client.getLoginDialogUrl(APP_ID, REDIRECT_URL, scopeBuilder); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Account)) { + return false; + } + + Account otherAccount = (Account) other; + return otherAccount.getUsername().equals(this.getUsername()) + && otherAccount.getPassword().equals(this.getPassword()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(username, password); + } + + @Override + public String toString() { + return "Username: " + getUsername() + " Password: " + getPassword(); + } +} diff --git a/src/main/java/seedu/address/model/account/Password.java b/src/main/java/seedu/address/model/account/Password.java new file mode 100644 index 000000000000..f8ef92265414 --- /dev/null +++ b/src/main/java/seedu/address/model/account/Password.java @@ -0,0 +1,58 @@ +package seedu.address.model.account; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author shadow2496 +/** + * Represents an Account's password in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPassword(String)} + */ +public class Password { + + public static final String MESSAGE_PASSWORD_CONSTRAINTS = + "Account passwords can take any values, and it should not be blank"; + + /* + * The first character of the password must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String PASSWORD_VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs an {@code Password}. + * + * @param password A valid password. + */ + public Password(String password) { + requireNonNull(password); + checkArgument(isValidPassword(password), MESSAGE_PASSWORD_CONSTRAINTS); + this.value = password; + } + + /** + * Returns true if a given string is a valid account password. + */ + public static boolean isValidPassword(String test) { + return test.matches(PASSWORD_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Password // instanceof handles nulls + && this.value.equals(((Password) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/account/Username.java b/src/main/java/seedu/address/model/account/Username.java new file mode 100644 index 000000000000..15881a27ffdd --- /dev/null +++ b/src/main/java/seedu/address/model/account/Username.java @@ -0,0 +1,58 @@ +package seedu.address.model.account; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author shadow2496 +/** + * Represents an Account's username in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidUsername(String)} + */ +public class Username { + + public static final String MESSAGE_USERNAME_CONSTRAINTS = + "Account usernames can take any values, and it should not be blank"; + + /* + * The first character of the username must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String USERNAME_VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs an {@code Username}. + * + * @param username A valid username. + */ + public Username(String username) { + requireNonNull(username); + checkArgument(isValidUsername(username), MESSAGE_USERNAME_CONSTRAINTS); + this.value = username; + } + + /** + * Returns true if a given string is a valid account username. + */ + public static boolean isValidUsername(String test) { + return test.matches(USERNAME_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Username // instanceof handles nulls + && this.value.equals(((Username) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 5e981f07790a..880d0acaf14a 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -32,7 +32,7 @@ public Address(String address) { } /** - * Returns true if a given string is a valid person email. + * Returns true if a given string is a valid person address. */ public static boolean isValidAddress(String test) { return test.matches(ADDRESS_VALIDATION_REGEX); diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index ec9f2aa5e919..c3e833124132 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -3,9 +3,12 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.Collections; +import java.util.Comparator; +import java.util.Map; import java.util.Objects; import java.util.Set; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; import seedu.address.model.tag.UniqueTagList; @@ -19,18 +22,21 @@ public class Person { private final Phone phone; private final Email email; private final Address address; + private final Map smpMap; private final UniqueTagList tags; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { + public Person(Name name, Phone phone, Email email, Address address, + Map socialMediaPlatformMap, Set tags) { requireAllNonNull(name, phone, email, address, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.smpMap = socialMediaPlatformMap; // protect internal tags from changes in the arg list this.tags = new UniqueTagList(tags); } @@ -51,6 +57,16 @@ public Address getAddress() { return address; } + //@@author Nethergale + /** + * Returns an immutable social media platform map, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Map getSocialMediaPlatformMap() { + return Collections.unmodifiableMap(smpMap); + } + + //@@author /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -59,6 +75,17 @@ public Set getTags() { return Collections.unmodifiableSet(tags.toSet()); } + //@@author Nethergale + /** + * Returns a person comparator, which compares the names alphabetically. + * Similar names are compared lexicographically. + */ + public static Comparator nameComparator() { + return Comparator.comparing((Person p) -> p.getName().toString(), ( + s1, s2) -> (s1.compareToIgnoreCase(s2) == 0) ? s1.compareTo(s2) : s1.compareToIgnoreCase(s2)); + } + + //@@author @Override public boolean equals(Object other) { if (other == this) { @@ -97,4 +124,18 @@ public String toString() { return builder.toString(); } + //@@author KevinChuangCH + /** + * Returns the tag names of tags of a person as String. + * @return a string of all the tag names of tags of a person. + */ + public String getTagsAsString() { + final StringBuilder builder = new StringBuilder(); + for (String tag : tags.arrayOfTags()) { + builder.append(tag); + builder.append(" "); + } + return builder.toString(); + } + } diff --git a/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java new file mode 100644 index 000000000000..03281317495f --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.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; + +//@@author KevinChuangCH +/** + * Tests that a {@code Person}'s {@code Tags} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getTagsAsString().trim(), keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && this.keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index f2c4c4c585e4..c1f0b98a576a 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -81,6 +81,15 @@ public boolean remove(Person toRemove) throws PersonNotFoundException { return personFoundAndDeleted; } + //@@author Nethergale + /** + * Sorts all persons in list alphabetically. Similar names are sorted lexicographically. + */ + public void sort() { + internalList.sort(Person.nameComparator()); + } + + //@@author public void setPersons(UniquePersonList replacement) { this.internalList.setAll(replacement.internalList); } diff --git a/src/main/java/seedu/address/model/smplatform/Facebook.java b/src/main/java/seedu/address/model/smplatform/Facebook.java new file mode 100644 index 000000000000..2cefd129c2ad --- /dev/null +++ b/src/main/java/seedu/address/model/smplatform/Facebook.java @@ -0,0 +1,21 @@ +package seedu.address.model.smplatform; + +//@@author Nethergale +/** + * Represents a facebook object. + */ +public class Facebook extends SocialMediaPlatform { + + public static final String PLATFORM_KEYWORD = "facebook"; + + public static final String PLATFORM_ALIAS = "fb"; + + //Code adapted from https://stackoverflow.com/questions/5205652/facebook-profile-url-regular-expression + public static final String LINK_VALIDATION_REGEX = "(?:https?:\\/\\/)?(?:www\\.|m\\.)?facebook\\.com\\/" + + "(?:profile.php\\?id=(?=\\d.*))?" + + "[^/ \\\\](?:(?:\\w)*#!\\/)?(?:pages\\/)?(?:[\\w\\-]*\\/)*([\\w\\-\\.]*)/?"; + + public Facebook(Link link) { + this.link = link; + } +} diff --git a/src/main/java/seedu/address/model/smplatform/Link.java b/src/main/java/seedu/address/model/smplatform/Link.java new file mode 100644 index 000000000000..9d037c1112b6 --- /dev/null +++ b/src/main/java/seedu/address/model/smplatform/Link.java @@ -0,0 +1,74 @@ +package seedu.address.model.smplatform; + +import static java.util.Objects.requireNonNull; + +//@@author Nethergale +/** + * Represents a SocialMediaPlatform's link. + * Guarantees: immutable; is always valid + */ +public class Link { + public static final String MESSAGE_LINK_CONSTRAINTS = "Only one link is allowed for each social media platform."; + public static final String MESSAGE_INVALID_LINK = "Links should be valid Facebook or Twitter profile links."; + public static final String FACEBOOK_LINK_TYPE = "facebook"; + public static final String TWITTER_LINK_TYPE = "twitter"; + public static final String UNKNOWN_LINK_TYPE = "unknown"; + + private static final String FACEBOOK_LINK_SIGNATURE = "facebook.com"; + private static final String TWITTER_LINK_SIGNATURE = "twitter.com"; + private static final String LONGEST_VALID_LINK_PREFIX = "https://www."; + + public final String value; + + public Link(String link) { + requireNonNull(link); + this.value = link; + } + + /** + * Returns the social media platform type of the link. + */ + public static String getLinkType(String link) { + if (link.contains(TWITTER_LINK_SIGNATURE) + && !(link.indexOf(TWITTER_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return TWITTER_LINK_TYPE; + } else if (link.contains(FACEBOOK_LINK_SIGNATURE) + && !(link.indexOf(FACEBOOK_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return FACEBOOK_LINK_TYPE; + } else { + return UNKNOWN_LINK_TYPE; + } + } + + /** + * Returns true if a given string is a valid link. + */ + public static boolean isValidLink(String test) { + if (test.contains(TWITTER_LINK_SIGNATURE) + && !(test.indexOf(TWITTER_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return test.matches(Twitter.LINK_VALIDATION_REGEX); + } else if (test.contains(FACEBOOK_LINK_SIGNATURE) + && !(test.indexOf(FACEBOOK_LINK_SIGNATURE) > LONGEST_VALID_LINK_PREFIX.length())) { + return test.matches(Facebook.LINK_VALIDATION_REGEX); + } else { + return false; + } + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Link // instanceof handles nulls + && this.value.equals(((Link) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/smplatform/SocialMediaPlatform.java b/src/main/java/seedu/address/model/smplatform/SocialMediaPlatform.java new file mode 100644 index 000000000000..513a9ebc65ff --- /dev/null +++ b/src/main/java/seedu/address/model/smplatform/SocialMediaPlatform.java @@ -0,0 +1,40 @@ +package seedu.address.model.smplatform; + +//@@author KevinChuangCH +/** + * Represents a social media platform, which can take many forms. + */ +public abstract class SocialMediaPlatform { + + public static final String MESSAGE_USERNAME_CONSTRAINTS = + "Social media usernames should only contain alphanumeric characters, " + + "underscore, and spaces, and it should not be blank"; + + public static final String USERNAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum}_ ]*"; + + public static final String MESSAGE_PLATFORM_CONSTRAINTS = + "Platforms available are : \n" + + "1) " + Facebook.PLATFORM_KEYWORD + " (alias: " + Facebook.PLATFORM_ALIAS + ")\n" + + "2) " + Twitter.PLATFORM_KEYWORD + " (alias: " + Twitter.PLATFORM_ALIAS + ")\n"; + + //@@author Nethergale + protected Link link; + + public Link getLink() { + return link; + } + + //@@author KevinChuangCH + /** + * Returns true if a given string is a valid social platform name. + */ + public static boolean isValidPlatform(String test) { + if (test.equals(Facebook.PLATFORM_KEYWORD) + || test.equals(Facebook.PLATFORM_ALIAS) + || test.equals(Twitter.PLATFORM_KEYWORD) + || test.equals(Twitter.PLATFORM_ALIAS)) { + return true; + } + return false; + } +} diff --git a/src/main/java/seedu/address/model/smplatform/SocialMediaPlatformFactory.java b/src/main/java/seedu/address/model/smplatform/SocialMediaPlatformFactory.java new file mode 100644 index 000000000000..d9ef34a3caf2 --- /dev/null +++ b/src/main/java/seedu/address/model/smplatform/SocialMediaPlatformFactory.java @@ -0,0 +1,35 @@ +package seedu.address.model.smplatform; + +import seedu.address.commons.exceptions.IllegalValueException; + +//@@author Nethergale +/** + * Acts as a social media platform creator. + * Determines the different types of social media platform objects to be created by using the link and its type. + */ +public final class SocialMediaPlatformFactory { + public static final String MESSAGE_BUILD_ERROR = "Social media platform cannot be constructed. " + + "Link type is unrecognised or mismatched with link."; + + /** + * Don't let anyone instantiate this class. + */ + private SocialMediaPlatformFactory() {} + + /** + * Constructs the specific social media platform object by using the {@code type} and setting the {@code link} + * as a parameter. + * + * @return the created social media platform object + * @throws IllegalValueException if type is not recognised + */ + public static SocialMediaPlatform getSocialMediaPlatform(String type, Link link) throws IllegalValueException { + if (type.equals(Link.FACEBOOK_LINK_TYPE) && type.equals(Link.getLinkType(link.value))) { + return new Facebook(link); + } else if (type.equals(Link.TWITTER_LINK_TYPE) && type.equals(Link.getLinkType(link.value))) { + return new Twitter(link); + } else { + throw new IllegalValueException(MESSAGE_BUILD_ERROR); + } + } +} diff --git a/src/main/java/seedu/address/model/smplatform/Twitter.java b/src/main/java/seedu/address/model/smplatform/Twitter.java new file mode 100644 index 000000000000..963808a9dc33 --- /dev/null +++ b/src/main/java/seedu/address/model/smplatform/Twitter.java @@ -0,0 +1,21 @@ +package seedu.address.model.smplatform; + +/** + * Represents a twitter object. + */ +public class Twitter extends SocialMediaPlatform { + + public static final String PLATFORM_KEYWORD = "twitter"; + + public static final String PLATFORM_ALIAS = "tw"; + + //@@author Nethergale-reused + //Reused from https://stackoverflow.com/questions/6024848/regex-to-validate-a-twitter-url with minor modifications + public static final String LINK_VALIDATION_REGEX = "(?:https?:\\/\\/)?(?:www\\.|m\\.)?twitter\\.com\\/" + + "[^/ \\\\](?:(?:\\w)*#!\\/)?(?:pages\\/)?(?:[\\w\\-]*\\/)*([\\w\\-]*)/?"; + + //@@author Nethergale + public Twitter(Link link) { + this.link = link; + } +} diff --git a/src/main/java/seedu/address/model/tag/UniqueTagList.java b/src/main/java/seedu/address/model/tag/UniqueTagList.java index e9a74947fc3f..01875d36ee48 100644 --- a/src/main/java/seedu/address/model/tag/UniqueTagList.java +++ b/src/main/java/seedu/address/model/tag/UniqueTagList.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -48,6 +49,21 @@ public Set toSet() { return new HashSet<>(internalList); } + //@@author KevinChuangCH + /** + * Returns an array of tag names of tags of a person. + * @return array of tag names + */ + public ArrayList arrayOfTags() { + assert CollectionUtil.elementsAreUnique(internalList); + ArrayList tagStringArray = new ArrayList<>(); + for (Tag t : internalList) { + tagStringArray.add(t.tagName); + } + return tagStringArray; + } + //@@author + /** * Replaces the Tags in this list with those in the argument tag list. */ diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index aea96bfb31f3..00ba11868aa7 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,6 +1,8 @@ package seedu.address.model.util; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import seedu.address.model.AddressBook; @@ -11,31 +13,40 @@ import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.smplatform.Facebook; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.smplatform.Twitter; import seedu.address.model.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + public static final Map EMPTY_SOCIAL_MEDIA_PLATFORM_MAP = new HashMap<>(); + public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), new Address("Blk 30 Geylang Street 29, #06-40"), + getSocialMediaPlatformMap("www.facebook.com/alex.yeo"), getTagSet("friends")), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + getSocialMediaPlatformMap("www.twitter.com/berniceyu"), getTagSet("colleagues", "friends")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + getSocialMediaPlatformMap("www.facebook.com/charlotte.oliverio", "www.twitter.com/charlotte"), getTagSet("neighbours")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_SOCIAL_MEDIA_PLATFORM_MAP, getTagSet("family")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), + new Address("Blk 47 Tampines Street 20, #17-35"), EMPTY_SOCIAL_MEDIA_PLATFORM_MAP, getTagSet("classmates")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), + new Address("Blk 45 Aljunied Street 85, #11-31"), EMPTY_SOCIAL_MEDIA_PLATFORM_MAP, getTagSet("colleagues")) }; } @@ -52,6 +63,27 @@ public static ReadOnlyAddressBook getSampleAddressBook() { } } + //@@author Nethergale + /** + * Returns a social media platform map mapping to the various platforms, + * using the list of strings given as the links. Unknown link types and invalid links will be ignored. + * Only the first link will be added if there are multiple links of the same type. + */ + public static Map getSocialMediaPlatformMap(String... strings) { + HashMap smpMap = new HashMap<>(); + for (String s : strings) { + String type = Link.getLinkType(s); + if (type.equals(Link.FACEBOOK_LINK_TYPE) && Link.isValidLink(s)) { + smpMap.putIfAbsent(type, new Facebook(new Link(s))); + } else if (type.equals(Link.TWITTER_LINK_TYPE) && Link.isValidLink(s)) { + smpMap.putIfAbsent(type, new Twitter(new Link(s))); + } + } + + return smpMap; + } + + //@@author /** * Returns a tag set containing the list of strings given. */ diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java index 2cd92dc4fd20..d91246a97d13 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java @@ -1,8 +1,10 @@ package seedu.address.storage; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -14,6 +16,8 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; /** @@ -32,6 +36,8 @@ public class XmlAdaptedPerson { @XmlElement(required = true) private String address; + @XmlElement + private List platforms = new ArrayList<>(); @XmlElement private List tagged = new ArrayList<>(); @@ -44,11 +50,15 @@ public XmlAdaptedPerson() {} /** * Constructs an {@code XmlAdaptedPerson} with the given person details. */ - public XmlAdaptedPerson(String name, String phone, String email, String address, List tagged) { + public XmlAdaptedPerson(String name, String phone, String email, String address, + List platforms, List tagged) { this.name = name; this.phone = phone; this.email = email; this.address = address; + if (platforms != null) { + this.platforms = new ArrayList<>(platforms); + } if (tagged != null) { this.tagged = new ArrayList<>(tagged); } @@ -64,6 +74,15 @@ public XmlAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + + //@@author Nethergale + platforms = new ArrayList<>(); + for (String key : source.getSocialMediaPlatformMap().keySet()) { + platforms.add( + new XmlAdaptedSocialMediaPlatform(key, source.getSocialMediaPlatformMap().get(key).getLink())); + } + + //@@author tagged = new ArrayList<>(); for (Tag tag : source.getTags()) { tagged.add(new XmlAdaptedTag(tag)); @@ -76,6 +95,14 @@ public XmlAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person */ public Person toModelType() throws IllegalValueException { + //@@author Nethergale + final Map personSocialMediaPlatforms = new HashMap<>(); + for (XmlAdaptedSocialMediaPlatform platform : platforms) { + SocialMediaPlatform platformModel = platform.toModelType(); + personSocialMediaPlatforms.put(Link.getLinkType(platformModel.getLink().value), platformModel); + } + + //@@author final List personTags = new ArrayList<>(); for (XmlAdaptedTag tag : tagged) { personTags.add(tag.toModelType()); @@ -114,7 +141,7 @@ public Person toModelType() throws IllegalValueException { final Address address = new Address(this.address); final Set tags = new HashSet<>(personTags); - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, personSocialMediaPlatforms, tags); } @Override @@ -132,6 +159,7 @@ public boolean equals(Object other) { && Objects.equals(phone, otherPerson.phone) && Objects.equals(email, otherPerson.email) && Objects.equals(address, otherPerson.address) + && Objects.equals(platforms, otherPerson.platforms) && tagged.equals(otherPerson.tagged); } } diff --git a/src/main/java/seedu/address/storage/XmlAdaptedSocialMediaPlatform.java b/src/main/java/seedu/address/storage/XmlAdaptedSocialMediaPlatform.java new file mode 100644 index 000000000000..d1775319991c --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAdaptedSocialMediaPlatform.java @@ -0,0 +1,71 @@ +package seedu.address.storage; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.smplatform.SocialMediaPlatformFactory; + +//@@author Nethergale +/** + * JAXB-friendly adapted version of the SocialMediaPlatform. + */ +public class XmlAdaptedSocialMediaPlatform { + + @XmlElement + private String type; + @XmlElement + private String link; + + /** + * Constructs an XmlAdaptedSocialMediaPlatform. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedSocialMediaPlatform() {} + + /** + * Constructs a {@code XmlAdaptedSocialMediaPlatform} with the given {@code type} and {@code link}. + */ + public XmlAdaptedSocialMediaPlatform(String type, String link) { + this.type = type; + this.link = link; + } + + /** + * Converts a given String and Link into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedSocialMediaPlatform + */ + public XmlAdaptedSocialMediaPlatform(String type, Link source) { + this.type = type; + link = source.value; + } + + /** + * Converts this jaxb-friendly adapted social media platform object into the model's social media platform object. + * + * @throws IllegalValueException if link is not valid + */ + public SocialMediaPlatform toModelType() throws IllegalValueException { + if (!Link.isValidLink(link)) { + throw new IllegalValueException(Link.MESSAGE_INVALID_LINK); + } + + return SocialMediaPlatformFactory.getSocialMediaPlatform(type, new Link(link)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedSocialMediaPlatform)) { + return false; + } + + return type.equals(((XmlAdaptedSocialMediaPlatform) other).type) + && link.equals(((XmlAdaptedSocialMediaPlatform) other).link); + } +} diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java index bb0d61380d5a..83ef5ef77256 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/address/ui/BrowserPanel.java @@ -1,19 +1,32 @@ package seedu.address.ui; import java.net.URL; +import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; import com.google.common.eventbus.Subscribe; import javafx.application.Platform; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Worker; import javafx.event.Event; import javafx.fxml.FXML; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.layout.Region; import javafx.scene.web.WebView; import seedu.address.MainApp; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; +import seedu.address.commons.events.ui.SearchPersonEvent; +import seedu.address.commons.events.ui.ShowLoginDialogRequestEvent; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.Logic; import seedu.address.model.person.Person; +import seedu.address.model.smplatform.Facebook; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.Twitter; /** * The Browser Panel of the App. @@ -21,19 +34,50 @@ public class BrowserPanel extends UiPart { public static final String DEFAULT_PAGE = "default.html"; - public static final String SEARCH_PAGE_URL = - "https://se-edu.github.io/addressbook-level4/DummySearchPage.html?name="; + public static final String LOADING_PAGE_URL = "https://cs2103jan2018-f12-b3.github.io/main/LoadingPage.html"; + public static final String FACEBOOK_SEARCH_PAGE_URL = + "https://www.facebook.com/search/people?q="; + public static final String TWITTER_SEARCH_PAGE_URL = + "https://twitter.com/search?f=users&vertical=news&q="; + + private static final String SUCCESS_URL = "https://www.facebook.com/connect/login_success.html"; private static final String FXML = "BrowserPanel.fxml"; + private static final String FACEBOOK_TAB_ID = "facebookTab"; + private static final String TWITTER_TAB_ID = "twitterTab"; + private static final String FUNCTION_ADD = "add"; + private static final String FUNCTION_REMOVE = "remove"; + + private URL defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + "default.html"); private final Logger logger = LogsCenter.getLogger(this.getClass()); + private final Logic logic; + + private Set openTabIdSet; + + @FXML + private TabPane tabPane; + + @FXML + private Tab facebookTab; + + @FXML + private Tab twitterTab; @FXML - private WebView browser; + private WebView facebookBrowser; - public BrowserPanel() { + @FXML + private WebView twitterBrowser; + + public BrowserPanel(Logic logic) { super(FXML); + this.logic = logic; + //@@author Nethergale + openTabIdSet = tabPane.getTabs().stream().map(tab -> tab.getId()).collect(Collectors.toSet()); + + //@@author // To prevent triggering events for typing inside the loaded Web page. getRoot().setOnKeyPressed(Event::consume); @@ -41,32 +85,213 @@ public BrowserPanel() { registerAsAnEventHandler(this); } - private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); + //@@author Nethergale + /** + * Updates the display of social media browser tabs. + * + * @param function defines an add or remove function + * @param tabId id of the fxml tab + */ + private void updateBrowserTabs(String function, String tabId) { + switch (function) { + + case FUNCTION_ADD: + addBrowserTab(tabId); + break; + + case FUNCTION_REMOVE: + removeBrowserTab(tabId); + break; + + default: + // Do nothing + } + } + + /** + * Adds the specified browser tab to the UI using the {@code tabId} if it is not open; + */ + private void addBrowserTab(String tabId) { + if (!openTabIdSet.contains(tabId)) { + openTabIdSet.add(tabId); + switch (tabId) { + + case FACEBOOK_TAB_ID: + tabPane.getTabs().add(0, facebookTab); + break; + + case TWITTER_TAB_ID: + tabPane.getTabs().add(twitterTab); + break; + + default: + // Do nothing + } + } + } + + /** + * Removes the specified browser tab from the UI using the {@code tabId}; + */ + private void removeBrowserTab(String tabId) { + openTabIdSet.remove(tabId); + switch (tabId) { + + case FACEBOOK_TAB_ID: + tabPane.getTabs().remove(facebookTab); + break; + + case TWITTER_TAB_ID: + tabPane.getTabs().remove(twitterTab); + break; + + default: + //Do nothing + } + } + + /** + * Returns the given {@code url} with a protocol and subdomain if unspecified. + */ + public static String parseUrl(String url) { + if (!url.contains("://")) { + if (!url.contains("www")) { + return "https://www." + url; + } + return "https://" + url; + } else { + if (!url.contains("www")) { + String[] splitUrl = url.split("://"); + return "https://www." + splitUrl[1]; + } + } + + return url; } - public void loadPage(String url) { - Platform.runLater(() -> browser.getEngine().load(url)); + /** + * Loads the Facebook profile page for the browser on the Facebook tab if it exists. + */ + private void loadFacebookBrowserProfilePage(Person person) { + if (person.getSocialMediaPlatformMap().containsKey(Link.FACEBOOK_LINK_TYPE)) { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + String url = person.getSocialMediaPlatformMap().get(Link.FACEBOOK_LINK_TYPE).getLink().value; + loadFacebookBrowserPage(parseUrl(url)); + } else { + updateBrowserTabs(FUNCTION_REMOVE, FACEBOOK_TAB_ID); + loadFacebookBrowserPage(defaultPage.toExternalForm()); + } + } + + /** + * Loads the Twitter profile page for the browser on the Twitter tab if it exists. + */ + private void loadTwitterBrowserProfilePage(Person person) { + if (person.getSocialMediaPlatformMap().containsKey(Link.TWITTER_LINK_TYPE)) { + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + String url = person.getSocialMediaPlatformMap().get(Link.TWITTER_LINK_TYPE).getLink().value; + loadTwitterBrowserPage(parseUrl(url)); + } else { + updateBrowserTabs(FUNCTION_REMOVE, TWITTER_TAB_ID); + loadTwitterBrowserPage(defaultPage.toExternalForm()); + } + } + + //@@author KevinChuangCH + private void loadBrowserSearchPage(String searchName) { + loadFacebookBrowserPage(FACEBOOK_SEARCH_PAGE_URL + searchName); + } + private void loadBrowser1SearchPage(String searchName) { + loadTwitterBrowserPage(TWITTER_SEARCH_PAGE_URL + searchName); + } + + //@@author + public void loadFacebookBrowserPage(String url) { + Platform.runLater(() -> facebookBrowser.getEngine().load(url)); + } + public void loadTwitterBrowserPage(String url) { + Platform.runLater(() -> twitterBrowser.getEngine().load(url)); } /** * Loads a default HTML file with a background that matches the general theme. */ private void loadDefaultPage() { - URL defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE); - loadPage(defaultPage.toExternalForm()); + loadFacebookBrowserPage(defaultPage.toExternalForm()); + loadTwitterBrowserPage(defaultPage.toExternalForm()); } + //@@author shadow2496 + /** + * Passes a verification code when the login is successful. + */ + private void passVerificationCode() { + facebookBrowser.getEngine().getLoadWorker().stateProperty().addListener(( + ObservableValue observable, Worker.State oldValue, Worker.State newValue) -> { + if (newValue != Worker.State.SUCCEEDED) { + return; + } + + String currentUrl = facebookBrowser.getEngine().getLocation(); + + if (currentUrl.endsWith(DEFAULT_PAGE)) { + } else if (currentUrl.startsWith(SUCCESS_URL)) { + int pos = currentUrl.indexOf("code="); + logic.passVerificationCode(currentUrl.substring(pos + "code=".length())); + } + }); + } + + //@@author + /** * Frees resources allocated to the browser. */ public void freeResources() { - browser = null; + facebookBrowser = null; + twitterBrowser = null; } @Subscribe private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - loadPersonPage(event.getNewSelection().person); + loadFacebookBrowserProfilePage(event.getNewSelection().person); + loadTwitterBrowserProfilePage(event.getNewSelection().person); + } + + //@@author KevinChuangCH + @Subscribe + private void handleSearchPersonEvent(SearchPersonEvent event) { + + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + + String platformToSearch = event.getPlatform(); + + if (StringUtil.containsWordIgnoreCase(platformToSearch, Facebook.PLATFORM_KEYWORD) + || StringUtil.containsWordIgnoreCase(platformToSearch, Facebook.PLATFORM_ALIAS)) { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + updateBrowserTabs(FUNCTION_REMOVE, TWITTER_TAB_ID); + loadBrowserSearchPage(event.getSearchName()); + loadTwitterBrowserPage(defaultPage.toExternalForm()); + } else if (StringUtil.containsWordIgnoreCase(platformToSearch, Twitter.PLATFORM_KEYWORD) + || StringUtil.containsWordIgnoreCase(platformToSearch, Twitter.PLATFORM_ALIAS)) { + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + updateBrowserTabs(FUNCTION_REMOVE, FACEBOOK_TAB_ID); + loadFacebookBrowserPage(defaultPage.toExternalForm()); + loadBrowser1SearchPage(event.getSearchName()); + } else { + updateBrowserTabs(FUNCTION_ADD, FACEBOOK_TAB_ID); + updateBrowserTabs(FUNCTION_ADD, TWITTER_TAB_ID); + loadBrowserSearchPage(event.getSearchName()); + loadBrowser1SearchPage(event.getSearchName()); + } + } + + //@@author shadow2496 + @Subscribe + private void handleShowLoginDialogRequestEvent(ShowLoginDialogRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + loadFacebookBrowserPage(event.loadUrl); + passVerificationCode(); } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9cef588df3c3..5adc654471a2 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -107,14 +107,14 @@ private void handleCommandInputChanged() { // process result of the command commandTextField.setText(""); logger.info("Result: " + commandResult.feedbackToUser); - raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser, false)); } catch (CommandException | ParseException e) { initHistory(); // handle command failure setStyleToIndicateCommandFailure(); logger.info("Invalid command: " + commandTextField.getText()); - raise(new NewResultAvailableEvent(e.getMessage())); + raise(new NewResultAvailableEvent(e.getMessage(), true)); } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 20ad5fee906a..03298d31c618 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -116,7 +116,7 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - browserPanel = new BrowserPanel(); + browserPanel = new BrowserPanel(logic); browserPlaceholder.getChildren().add(browserPanel.getRoot()); personListPanel = new PersonListPanel(logic.getFilteredPersonList()); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..1ad5ad635806 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,10 +2,16 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + import seedu.address.model.person.Person; +import seedu.address.model.smplatform.Link; /** * An UI component that displays information of a {@code Person}. @@ -37,6 +43,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private VBox socialMediaIconPane; + @FXML private FlowPane tags; public PersonCard(Person person, int displayedIndex) { @@ -47,6 +55,7 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + addIconsToSocialMediaIconPane(); person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } @@ -67,4 +76,53 @@ public boolean equals(Object other) { return id.getText().equals(card.id.getText()) && person.equals(card.person); } + + //@@author Nethergale + /** + * Adds the various social media icon tab to the social media icon pane. + */ + private void addIconsToSocialMediaIconPane() { + for (String key : person.getSocialMediaPlatformMap().keySet()) { + if (!person.getSocialMediaPlatformMap().get(key).getLink().value.isEmpty()) { + socialMediaIconPane.getChildren().add(createSocialMediaIconTab(key)); + } + } + } + + /** + * Creates the icon tab for the specific social media platform. + * + * @param type social media platform type + * @return stack pane representing an icon tab + */ + private StackPane createSocialMediaIconTab(String type) { + StackPane socialMediaIconTab = new StackPane(); + Region tabBackground = new Region(); + tabBackground.setPrefSize(30, 32); + tabBackground.setStyle("-fx-background-color: linear-gradient(from 0% 0% to 100% 0%, #29323C, #485563); " + + "-fx-background-radius: 6 0 0 6; -fx-border-color: #242C35; " + + "-fx-border-radius: 6 0 0 6; -fx-border-width: 2 0 2 2;"); + ImageView tabIcon = new ImageView(); + String url = imageUrl(type); + if (!url.isEmpty()) { + tabIcon.setFitWidth(20); + tabIcon.setFitHeight(20); + tabIcon.setImage(new Image(url)); + socialMediaIconTab.getChildren().addAll(tabBackground, tabIcon); + } + return socialMediaIconTab; + } + + /** + * Returns the location of the icon for the specific social media platform {@code type}. + */ + private String imageUrl(String type) { + if (type.equals(Link.FACEBOOK_LINK_TYPE)) { + return "images/facebook_icon.png"; + } else if (type.equals(Link.TWITTER_LINK_TYPE)) { + return "images/twitter_icon.png"; + } else { + return ""; + } + } } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index d05536bbee96..fbe7f36a0752 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -7,6 +7,7 @@ import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextArea; import javafx.scene.layout.Region; @@ -18,6 +19,7 @@ */ public class ResultDisplay extends UiPart { + public static final String ERROR_STYLE_CLASS = "error"; private static final Logger logger = LogsCenter.getLogger(ResultDisplay.class); private static final String FXML = "ResultDisplay.fxml"; @@ -36,6 +38,35 @@ public ResultDisplay() { private void handleNewResultAvailableEvent(NewResultAvailableEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); Platform.runLater(() -> displayed.setValue(event.message)); + + //@@author shadow2496 + if (event.hasError) { + setStyleToIndicateResultError(); + } else { + setStyleToDefault(); + } + + //@@author + } + + //@@author shadow2496 + /** + * Sets the result display style to use the default style. + */ + private void setStyleToDefault() { + resultDisplay.getStyleClass().remove(ERROR_STYLE_CLASS); } + /** + * Sets the result display style to indicate that a result has an error. + */ + private void setStyleToIndicateResultError() { + ObservableList styleClass = resultDisplay.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } } diff --git a/src/main/resources/images/facebook_icon.png b/src/main/resources/images/facebook_icon.png new file mode 100644 index 000000000000..bea2a02b615d Binary files /dev/null and b/src/main/resources/images/facebook_icon.png differ diff --git a/src/main/resources/images/twitter_icon.png b/src/main/resources/images/twitter_icon.png new file mode 100644 index 000000000000..8f214a7811a8 Binary files /dev/null and b/src/main/resources/images/twitter_icon.png differ diff --git a/src/main/resources/view/BrowserPanel.fxml b/src/main/resources/view/BrowserPanel.fxml index 31670827e3da..01d0f55ea545 100644 --- a/src/main/resources/view/BrowserPanel.fxml +++ b/src/main/resources/view/BrowserPanel.fxml @@ -1,8 +1,29 @@ - + + + - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad558..feec9f75b363 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -12,11 +12,16 @@ + - + + + + + - + diff --git a/src/test/data/XmlUtilTest/missingPersonField.xml b/src/test/data/XmlUtilTest/missingPersonField.xml index c0da5c86d080..2d8fff0fc594 100644 --- a/src/test/data/XmlUtilTest/missingPersonField.xml +++ b/src/test/data/XmlUtilTest/missingPersonField.xml @@ -4,5 +4,9 @@ 9482424 hans@example
4th street
+ + facebook + www.facebook.com/hans.muster + friends
diff --git a/src/test/data/XmlUtilTest/validPerson.xml b/src/test/data/XmlUtilTest/validPerson.xml index c029008d54f4..927561344b78 100644 --- a/src/test/data/XmlUtilTest/validPerson.xml +++ b/src/test/data/XmlUtilTest/validPerson.xml @@ -4,5 +4,9 @@ 9482424 hans@example
4th street
+ + facebook + www.facebook.com/hans.muster + friends
diff --git a/src/test/java/guitests/GuiRobot.java b/src/test/java/guitests/GuiRobot.java index 965e6ebed63c..8fbe9186e313 100644 --- a/src/test/java/guitests/GuiRobot.java +++ b/src/test/java/guitests/GuiRobot.java @@ -15,7 +15,7 @@ public class GuiRobot extends FxRobot { private static final int PAUSE_FOR_HUMAN_DELAY_MILLISECONDS = 250; - private static final int DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS = 5000; + private static final int DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MILLISECONDS = 8000; private static final String PROPERTY_TESTFX_HEADLESS = "testfx.headless"; diff --git a/src/test/java/guitests/guihandles/BrowserPanelHandle.java b/src/test/java/guitests/guihandles/BrowserPanelHandle.java index bd3633af78f3..5f332ecca79b 100644 --- a/src/test/java/guitests/guihandles/BrowserPanelHandle.java +++ b/src/test/java/guitests/guihandles/BrowserPanelHandle.java @@ -1,5 +1,6 @@ package guitests.guihandles; +import java.net.MalformedURLException; import java.net.URL; import guitests.GuiRobot; @@ -7,24 +8,44 @@ import javafx.scene.Node; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; +import seedu.address.model.smplatform.Link; +import seedu.address.ui.BrowserPanel; /** * A handler for the {@code BrowserPanel} of the UI. */ public class BrowserPanelHandle extends NodeHandle { - public static final String BROWSER_ID = "#browser"; + //@@author Nethergale + public static final String FACEBOOK_BROWSER_ID = "#facebookBrowser"; + public static final String TWITTER_BROWSER_ID = "#twitterBrowser"; + public static final String TAB_PANE_ID = "#tabPane"; private boolean isWebViewLoaded = true; private URL lastRememberedUrl; + private WebView facebookWebView; + private WebView twitterWebView; + public BrowserPanelHandle(Node browserPanelNode) { super(browserPanelNode); - WebView webView = getChildNode(BROWSER_ID); - WebEngine engine = webView.getEngine(); - new GuiRobot().interact(() -> engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + facebookWebView = getChildNode(FACEBOOK_BROWSER_ID); // browser for facebookTab + WebEngine facebookEngine = facebookWebView.getEngine(); + new GuiRobot().interact(() -> facebookEngine.getLoadWorker().stateProperty().addListener(( + obs, oldState, newState) -> { + if (newState == Worker.State.RUNNING) { + isWebViewLoaded = false; + } else if (newState == Worker.State.SUCCEEDED) { + isWebViewLoaded = true; + } + })); + + twitterWebView = getChildNode(TWITTER_BROWSER_ID); // browser for twitterTab + WebEngine twitterEngine = twitterWebView.getEngine(); + new GuiRobot().interact(() -> twitterEngine.getLoadWorker().stateProperty().addListener(( + obs, oldState, newState) -> { if (newState == Worker.State.RUNNING) { isWebViewLoaded = false; } else if (newState == Worker.State.SUCCEEDED) { @@ -34,12 +55,42 @@ public BrowserPanelHandle(Node browserPanelNode) { } /** - * Returns the {@code URL} of the currently loaded page. + * Returns the {@code URL} of the currently loaded page for the default browser tab (i.e. facebookTab). */ public URL getLoadedUrl() { - return WebViewUtil.getLoadedUrl(getChildNode(BROWSER_ID)); + URL loadedUrl = WebViewUtil.getLoadedUrl(twitterWebView); + if (Link.isValidLink(loadedUrl.toExternalForm())) { + String completeUrl = BrowserPanel.parseUrl(loadedUrl.toExternalForm()); + try { + loadedUrl = new URL(completeUrl); + return loadedUrl; + } catch (MalformedURLException mue) { + throw new AssertionError("URL expected to be valid."); + } + } + return WebViewUtil.getLoadedUrl(facebookWebView); + } + + /** + * Returns the {@code URL} of the currently loaded page for the specified {@code browserTab}. + */ + public URL getLoadedUrl(String browserTab) { + if (browserTab.equals(Link.TWITTER_LINK_TYPE)) { + URL loadedUrl = WebViewUtil.getLoadedUrl(twitterWebView); + if (Link.isValidLink(loadedUrl.toExternalForm())) { + String completeUrl = BrowserPanel.parseUrl(loadedUrl.toExternalForm()); + try { + loadedUrl = new URL(completeUrl); + return loadedUrl; + } catch (MalformedURLException mue) { + throw new AssertionError("URL expected to be valid."); + } + } + } + return getLoadedUrl(); } + //@@author /** * Remembers the {@code URL} of the currently loaded page. */ diff --git a/src/test/java/guitests/guihandles/MainWindowHandle.java b/src/test/java/guitests/guihandles/MainWindowHandle.java index 34e36054f4fd..c0d813e529ff 100644 --- a/src/test/java/guitests/guihandles/MainWindowHandle.java +++ b/src/test/java/guitests/guihandles/MainWindowHandle.java @@ -22,7 +22,7 @@ public MainWindowHandle(Stage stage) { commandBox = new CommandBoxHandle(getChildNode(CommandBoxHandle.COMMAND_INPUT_FIELD_ID)); statusBarFooter = new StatusBarFooterHandle(getChildNode(StatusBarFooterHandle.STATUS_BAR_PLACEHOLDER)); mainMenu = new MainMenuHandle(getChildNode(MainMenuHandle.MENU_BAR_ID)); - browserPanel = new BrowserPanelHandle(getChildNode(BrowserPanelHandle.BROWSER_ID)); + browserPanel = new BrowserPanelHandle(getChildNode(BrowserPanelHandle.TAB_PANE_ID)); } public PersonListPanelHandle getPersonListPanel() { diff --git a/src/test/java/guitests/guihandles/ResultDisplayHandle.java b/src/test/java/guitests/guihandles/ResultDisplayHandle.java index ec445e4154ff..c18bad633d4a 100644 --- a/src/test/java/guitests/guihandles/ResultDisplayHandle.java +++ b/src/test/java/guitests/guihandles/ResultDisplayHandle.java @@ -1,5 +1,6 @@ package guitests.guihandles; +import javafx.collections.ObservableList; import javafx.scene.control.TextArea; /** @@ -19,4 +20,12 @@ public ResultDisplayHandle(TextArea resultDisplayNode) { public String getText() { return getRootNode().getText(); } + + //@@author shadow2496 + /** + * Returns the list of style classes present in the result display. + */ + public ObservableList getStyleClass() { + return getRootNode().getStyleClass(); + } } diff --git a/src/test/java/seedu/address/commons/core/ConfigTest.java b/src/test/java/seedu/address/commons/core/ConfigTest.java index cc4bf567cb44..184f479f9712 100644 --- a/src/test/java/seedu/address/commons/core/ConfigTest.java +++ b/src/test/java/seedu/address/commons/core/ConfigTest.java @@ -14,7 +14,7 @@ public class ConfigTest { @Test public void toString_defaultObject_stringReturned() { - String defaultConfigAsString = "App title : Address App\n" + String defaultConfigAsString = "App title : Media Socializer\n" + "Current log level : INFO\n" + "Preference file Location : preferences.json"; diff --git a/src/test/java/seedu/address/commons/util/XmlUtilTest.java b/src/test/java/seedu/address/commons/util/XmlUtilTest.java index 56b6ef8f40d3..d31031f0da35 100644 --- a/src/test/java/seedu/address/commons/util/XmlUtilTest.java +++ b/src/test/java/seedu/address/commons/util/XmlUtilTest.java @@ -16,6 +16,7 @@ import seedu.address.model.AddressBook; import seedu.address.storage.XmlAdaptedPerson; +import seedu.address.storage.XmlAdaptedSocialMediaPlatform; import seedu.address.storage.XmlAdaptedTag; import seedu.address.storage.XmlSerializableAddressBook; import seedu.address.testutil.AddressBookBuilder; @@ -39,6 +40,8 @@ public class XmlUtilTest { private static final String VALID_PHONE = "9482424"; private static final String VALID_EMAIL = "hans@example"; private static final String VALID_ADDRESS = "4th street"; + private static final List VALID_PLATFORMS = Collections.singletonList( + new XmlAdaptedSocialMediaPlatform("facebook", "www.facebook.com/hans.muster")); private static final List VALID_TAGS = Collections.singletonList(new XmlAdaptedTag("friends")); @Rule @@ -80,7 +83,7 @@ public void xmlAdaptedPersonFromFile_fileWithMissingPersonField_validResult() th XmlAdaptedPerson actualPerson = XmlUtil.getDataFromFile( MISSING_PERSON_FIELD_FILE, XmlAdaptedPersonWithRootElement.class); XmlAdaptedPerson expectedPerson = new XmlAdaptedPerson( - null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); assertEquals(expectedPerson, actualPerson); } @@ -89,7 +92,7 @@ public void xmlAdaptedPersonFromFile_fileWithInvalidPersonField_validResult() th XmlAdaptedPerson actualPerson = XmlUtil.getDataFromFile( INVALID_PERSON_FIELD_FILE, XmlAdaptedPersonWithRootElement.class); XmlAdaptedPerson expectedPerson = new XmlAdaptedPerson( - VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); assertEquals(expectedPerson, actualPerson); } @@ -98,7 +101,7 @@ public void xmlAdaptedPersonFromFile_fileWithValidPerson_validResult() throws Ex XmlAdaptedPerson actualPerson = XmlUtil.getDataFromFile( VALID_PERSON_FILE, XmlAdaptedPersonWithRootElement.class); XmlAdaptedPerson expectedPerson = new XmlAdaptedPerson( - VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); assertEquals(expectedPerson, actualPerson); } diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 461cf09d1217..99f54d91a02b 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -21,6 +21,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.account.Account; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -112,6 +113,19 @@ public ReadOnlyAddressBook getAddressBook() { return null; } + //@@author shadow2496 + @Override + public void loginAccount(Account account) { + fail("This method should not be called."); + } + + @Override + public void setVerificationCode(String code) { + fail("This method should not be called."); + } + + //@@author + @Override public void deletePerson(Person target) throws PersonNotFoundException { fail("This method should not be called."); @@ -123,6 +137,11 @@ public void updatePerson(Person target, Person editedPerson) fail("This method should not be called."); } + @Override + public void sortAllPersons() { + fail("This method should not be called."); + } + @Override public ObservableList getFilteredPersonList() { fail("This method should not be called."); diff --git a/src/test/java/seedu/address/logic/commands/AddPlatformCommandTest.java b/src/test/java/seedu/address/logic/commands/AddPlatformCommandTest.java new file mode 100644 index 000000000000..2ded16070820 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddPlatformCommandTest.java @@ -0,0 +1,282 @@ +package seedu.address.logic.commands; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.SMP_MAP_AMY; +import static seedu.address.logic.commands.CommandTestUtil.SMP_MAP_BOB; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.prepareRedoCommand; +import static seedu.address.logic.commands.CommandTestUtil.prepareUndoCommand; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.model.smplatform.Facebook; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.smplatform.SocialMediaPlatformFactory; +import seedu.address.model.util.SampleDataUtil; +import seedu.address.testutil.PersonBuilder; + +//@@author Nethergale +public class AddPlatformCommandTest { + public static final String LINK_STUB = "www.facebook.com/carl.kz"; + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_invalidPlatformLink_failure() { + String invalidLink = "www.google.com"; + Map smpMap = new HashMap<>(); + smpMap.put(Link.FACEBOOK_LINK_TYPE, new Facebook(new Link(invalidLink))); + + AddPlatformCommand addPlatformCommand = prepareCommand(INDEX_THIRD_PERSON, smpMap); + + assertCommandFailure(addPlatformCommand, model, SocialMediaPlatformFactory.MESSAGE_BUILD_ERROR); + } + + @Test + public void execute_addPlatformUnfilteredList_success() throws Exception { + Person thirdPerson = model.getFilteredPersonList().get(INDEX_THIRD_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(thirdPerson).withPlatforms(LINK_STUB).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_THIRD_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(thirdPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_retainAndAddPlatformsUnfilteredList_success() throws Exception { + Person secondPerson = model.getFilteredPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); + Set linkSet = new LinkedHashSet<>(); + linkSet.add(LINK_STUB); + for (String key : secondPerson.getSocialMediaPlatformMap().keySet()) { + linkSet.add(secondPerson.getSocialMediaPlatformMap().get(key).getLink().value); + } + Person editedPerson = new PersonBuilder(secondPerson) + .withPlatforms(linkSet.toArray(new String[linkSet.size()])).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_SECOND_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(secondPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_clearAllPlatformsUnfilteredList_success() throws Exception { + Person thirdPerson = model.getFilteredPersonList().get(INDEX_THIRD_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(thirdPerson).withPlatforms("").build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_THIRD_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = + String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_CLEAR_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(thirdPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_addPlatformFilteredList_success() throws Exception { + showPersonAtIndex(model, INDEX_THIRD_PERSON); + + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())) + .withPlatforms(LINK_STUB).build(); + + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_FIRST_PERSON, editedPerson.getSocialMediaPlatformMap()); + + String expectedMessage = String.format(AddPlatformCommand.MESSAGE_ADD_PLATFORM_SUCCESS, editedPerson.getName()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.updatePerson(firstPerson, editedPerson); + + assertCommandSuccess(addPlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_BOB); + + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book. + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + + //Ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_AMY); + + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeUndoRedo_validIndexUnfilteredList_success() throws Exception { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms(LINK_STUB).build(); + AddPlatformCommand addPlatformCommand = + prepareCommand(INDEX_FIRST_PERSON, editedPerson.getSocialMediaPlatformMap()); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + // addplatform -> first person platforms changed + addPlatformCommand.execute(); + undoRedoStack.push(addPlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + // redo -> same first person modified again + expectedModel.updatePerson(personToEdit, editedPerson); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_invalidIndexUnfilteredList_failure() { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + AddPlatformCommand addPlatformCommand = prepareCommand(outOfBoundIndex, SMP_MAP_AMY); + + // execution failed -> addPlatformCommand not pushed into undoRedoStack + assertCommandFailure(addPlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + + // no commands in undoRedoStack -> undoCommand and redoCommand fail + assertCommandFailure(undoCommand, model, UndoCommand.MESSAGE_FAILURE); + assertCommandFailure(redoCommand, model, RedoCommand.MESSAGE_FAILURE); + } + + /** + * 1. Modifies a person's social media platform from a filtered list. + * 2. Undo the modification. + * 3. The unfiltered list should be shown now. Verify that the index of the previously modified person in the + * unfiltered list is different from the index at the filtered list. + * 4. Redo the modification. This ensures {@code RedoCommand} modifies the person object regardless of indexing. + */ + @Test + public void executeUndoRedo_validIndexFilteredList_samePersonModified() throws Exception { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Set linkSet = new LinkedHashSet<>(); + linkSet.add(LINK_STUB); + for (String key : firstPerson.getSocialMediaPlatformMap().keySet()) { + linkSet.add(firstPerson.getSocialMediaPlatformMap().get(key).getLink().toString()); + } + + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + AddPlatformCommand addPlatformCommand = prepareCommand(INDEX_FIRST_PERSON, + SampleDataUtil.getSocialMediaPlatformMap(linkSet.toArray(new String[linkSet.size()]))); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + showPersonAtIndex(model, INDEX_SECOND_PERSON); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit) + .withPlatforms(linkSet.toArray(new String[linkSet.size()])).build(); + + // addplatform -> modifies second person in unfiltered person list / first person in filtered person list + addPlatformCommand.execute(); + undoRedoStack.push(addPlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + expectedModel.updatePerson(personToEdit, editedPerson); + assertNotEquals(personToEdit, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + // redo -> modifies same second person in unfiltered person list + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equals() { + Map amyLinkMap = new HashMap<>(); + Map bobLinkMap = new HashMap<>(); + for (String key : SMP_MAP_AMY.keySet()) { + amyLinkMap.put(key, SMP_MAP_AMY.get(key).getLink()); + } + for (String key : SMP_MAP_BOB.keySet()) { + amyLinkMap.put(key, SMP_MAP_BOB.get(key).getLink()); + } + + final AddPlatformCommand standardCommand = new AddPlatformCommand(INDEX_FIRST_PERSON, amyLinkMap); + + // same values -> returns true + AddPlatformCommand commandWithSameValues = new AddPlatformCommand(INDEX_FIRST_PERSON, amyLinkMap); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new AddPlatformCommand(INDEX_SECOND_PERSON, amyLinkMap))); + + // different remark -> returns false + assertFalse(standardCommand.equals(new AddPlatformCommand(INDEX_FIRST_PERSON, bobLinkMap))); + } + + /** + * Returns an {@code AddPlatformCommand} with parameters {@code index} and {@code Map}. + */ + private AddPlatformCommand prepareCommand(Index index, Map smpMap) { + Map linkMap = new HashMap<>(); + for (String key : smpMap.keySet()) { + linkMap.put(key, smpMap.get(key).getLink()); + } + AddPlatformCommand addPlatformCommand = new AddPlatformCommand(index, linkMap); + addPlatformCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + return addPlatformCommand; + } +} diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 9a5679cc29b6..4b6cbf238780 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -6,12 +6,15 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; 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_PASSWORD; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import seedu.address.commons.core.index.Index; import seedu.address.logic.CommandHistory; @@ -22,6 +25,8 @@ import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.util.SampleDataUtil; import seedu.address.testutil.EditPersonDescriptorBuilder; /** @@ -29,6 +34,11 @@ */ public class CommandTestUtil { + public static final String VALID_USERNAME_AMY = "amybee"; + public static final String VALID_USERNAME_BOB = "bobchoo"; + public static final String VALID_PASSWORD_AMY = "amy1111"; + public static final String VALID_PASSWORD_BOB = "bob2222"; + public static final String VALID_NAME_AMY = "Amy Bee"; public static final String VALID_NAME_BOB = "Bob Choo"; public static final String VALID_PHONE_AMY = "11111111"; @@ -37,9 +47,17 @@ public class CommandTestUtil { public static final String VALID_EMAIL_BOB = "bob@example.com"; public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_FACEBOOK_LINK_AMY = "www.facebook.com/a.my"; + public static final String VALID_TWITTER_LINK_AMY = "www.twitter.com/amy"; + public static final String VALID_TWITTER_LINK_BOB = "www.twitter.com/bob"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; + public static final String USERNAME_DESC_AMY = " " + PREFIX_USERNAME + VALID_USERNAME_AMY; + public static final String USERNAME_DESC_BOB = " " + PREFIX_USERNAME + VALID_USERNAME_BOB; + public static final String PASSWORD_DESC_AMY = " " + PREFIX_PASSWORD + VALID_PASSWORD_AMY; + public static final String PASSWORD_DESC_BOB = " " + PREFIX_PASSWORD + VALID_PASSWORD_BOB; + public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; @@ -51,6 +69,9 @@ public class CommandTestUtil { public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; + public static final String INVALID_USERNAME_DESC = " " + PREFIX_USERNAME; // empty string not allowed for username + public static final String INVALID_PASSWORD_DESC = " " + PREFIX_PASSWORD; // empty string not allowed for password + public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol @@ -63,6 +84,9 @@ public class CommandTestUtil { public static final EditCommand.EditPersonDescriptor DESC_AMY; public static final EditCommand.EditPersonDescriptor DESC_BOB; + public static final Map SMP_MAP_AMY; + public static final Map SMP_MAP_BOB; + static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) @@ -70,6 +94,9 @@ public class CommandTestUtil { DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + + SMP_MAP_AMY = SampleDataUtil.getSocialMediaPlatformMap(VALID_FACEBOOK_LINK_AMY, VALID_TWITTER_LINK_AMY); + SMP_MAP_BOB = SampleDataUtil.getSocialMediaPlatformMap(VALID_TWITTER_LINK_BOB); } /** diff --git a/src/test/java/seedu/address/logic/commands/FindWithTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindWithTagCommandTest.java new file mode 100644 index 000000000000..0999ec8b0010 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindWithTagCommandTest.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.model.person.TagContainsKeywordsPredicate; + +//@@author KevinChuangCH +/** + * Contains integration tests (interaction with the Model) for {@code FindWithTagCommand}. + */ +public class FindWithTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + TagContainsKeywordsPredicate firstPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("first")); + TagContainsKeywordsPredicate secondPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("second")); + + FindWithTagCommand findFirstCommand = new FindWithTagCommand(firstPredicate); + FindWithTagCommand findSecondCommand = new FindWithTagCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FindWithTagCommand findFirstCommandCopy = new FindWithTagCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + FindWithTagCommand command = prepareCommand(" "); + assertCommandSuccess(command, expectedMessage, Collections.emptyList()); + } + + @Test + public void execute_multipleKeywords_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + FindWithTagCommand command = prepareCommand("classmate PC3196 labPartner"); + assertCommandSuccess(command, expectedMessage, Arrays.asList(CARL, ELLE, FIONA)); + } + + /** + * Parses {@code userInput} into a {@code FindWithTagCommand}. + */ + private FindWithTagCommand prepareCommand(String userInput) { + FindWithTagCommand command = + new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+")))); + command.setData(model, new CommandHistory(), new UndoRedoStack()); + return command; + } + + /** + * Asserts that {@code command} is successfully executed, and
+ * - the command feedback is equal to {@code expectedMessage}
+ * - the {@code FilteredList} is equal to {@code expectedList}
+ * - the {@code AddressBook} in model remains the same after executing the {@code command} + */ + private void assertCommandSuccess(FindWithTagCommand command, String expectedMessage, List expectedList) { + AddressBook expectedAddressBook = new AddressBook(model.getAddressBook()); + CommandResult commandResult = command.execute(); + + assertEquals(expectedMessage, commandResult.feedbackToUser); + assertEquals(expectedList, model.getFilteredPersonList()); + assertEquals(expectedAddressBook, model.getAddressBook()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/LoginCommandTest.java b/src/test/java/seedu/address/logic/commands/LoginCommandTest.java new file mode 100644 index 000000000000..fd7b511e0601 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/LoginCommandTest.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PASSWORD_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PASSWORD_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_USERNAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_USERNAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.Test; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.account.Account; +import seedu.address.testutil.AccountBuilder; + +//@@author shadow2496 +public class LoginCommandTest { + + @Test + public void execute_login_success() { + Account validAccount = new AccountBuilder().build(); + LoginCommand command = new LoginCommand(validAccount); + + String expectedMessage = String.format(LoginCommand.MESSAGE_SUCCESS, validAccount); + + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + command.setData(model, new CommandHistory(), new UndoRedoStack()); + + assertCommandSuccess(command, model, expectedMessage, model); + } + + @Test + public void equals() { + Account account = new AccountBuilder().withUsername(VALID_USERNAME_AMY) + .withPassword(VALID_PASSWORD_AMY).build(); + Account accountWithDiffUsername = new AccountBuilder().withUsername(VALID_USERNAME_BOB) + .withPassword(VALID_PASSWORD_AMY).build(); + Account accountWithDiffPassword = new AccountBuilder().withUsername(VALID_USERNAME_AMY) + .withPassword(VALID_PASSWORD_BOB).build(); + LoginCommand standardCommand = new LoginCommand(account); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // same values -> returns true + LoginCommand commandWithSameValues = new LoginCommand(account); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // different types -> returns false + assertFalse(standardCommand.equals(1)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different values -> returns false + LoginCommand commandWithDiffUsername = new LoginCommand(accountWithDiffUsername); + LoginCommand commandWithDiffPassword = new LoginCommand(accountWithDiffPassword); + assertFalse(standardCommand.equals(commandWithDiffUsername)); + assertFalse(standardCommand.equals(commandWithDiffPassword)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/RemovePlatformCommandTest.java b/src/test/java/seedu/address/logic/commands/RemovePlatformCommandTest.java new file mode 100644 index 000000000000..2e57c23532bb --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/RemovePlatformCommandTest.java @@ -0,0 +1,267 @@ +package seedu.address.logic.commands; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.prepareRedoCommand; +import static seedu.address.logic.commands.CommandTestUtil.prepareUndoCommand; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.junit.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.smplatform.Link; +import seedu.address.testutil.PersonBuilder; + +//@@author Nethergale +/** + * Contains integration tests (interaction with the Model, UndoCommand and RedoCommand) and unit tests for + * {@code RemovePlatformCommand}. + */ +public class RemovePlatformCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_removePlatformsWithPlatformFields_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.FACEBOOK_LINK_TYPE); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithPlatformFieldsDifferentCasing_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.FACEBOOK_LINK_TYPE.toUpperCase()); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithSomeUnrecognisedPlatformFields_success() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add("random"); + platformSet.add(Link.FACEBOOK_LINK_TYPE); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_removePlatformsWithAllUnrecognisedPlatformFields_failure() { + Set platformSet = new HashSet<>(); + platformSet.add(""); + platformSet.add("hello"); + platformSet.add("tester"); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + + assertCommandFailure(removePlatformCommand, model, RemovePlatformCommand.MESSAGE_PLATFORM_MAP_NOT_EDITED); + } + + @Test + public void execute_validIndexUnfilteredList_success() throws Exception { + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() throws Exception { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + + String expectedMessage = + String.format(RemovePlatformCommand.MESSAGE_REMOVE_PLATFORM_SUCCESS, personToEdit.getName()); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.updatePerson(personToEdit, editedPerson); + + //Update expectedModel to be filtered + String[] splitName = personToEdit.getName().fullName.split("\\s+"); + Predicate predicate = new NameContainsKeywordsPredicate(Arrays.asList(splitName[0])); + expectedModel.updateFilteredPersonList(predicate); + + assertCommandSuccess(removePlatformCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeUndoRedo_validIndexUnfilteredList_success() throws Exception { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + // removeplatform -> first person's platforms removed + removePlatformCommand.execute(); + undoRedoStack.push(removePlatformCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + // redo -> same first person's platforms deleted again + expectedModel.updatePerson(personToEdit, editedPerson); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_invalidIndexUnfilteredList_failure() { + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + RemovePlatformCommand removePlatformCommand = prepareCommand(outOfBoundIndex, new HashSet<>()); + + // execution failed -> removePlatformCommand not pushed into undoRedoStack + assertCommandFailure(removePlatformCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + + // no commands in undoRedoStack -> undoCommand and redoCommand fail + assertCommandFailure(undoCommand, model, UndoCommand.MESSAGE_FAILURE); + assertCommandFailure(redoCommand, model, RedoCommand.MESSAGE_FAILURE); + } + + /** + * 1. Modifies a {@code Person} from a filtered list by removing the stated social media platform. + * 2. Undo the modification. + * 3. The unfiltered list should be shown now. Verify that the index of the previously modified person in the + * unfiltered list is different from the index at the filtered list. + * 4. Redo the modification. This ensures {@code RedoCommand} modifies the person object regardless of indexing. + */ + @Test + public void executeUndoRedo_validIndexFilteredList_samePersonModified() throws Exception { + Set platformSet = new HashSet<>(); + platformSet.add(Link.TWITTER_LINK_TYPE); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + RemovePlatformCommand removePlatformCommand = prepareCommand(INDEX_FIRST_PERSON, platformSet); + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + + showPersonAtIndex(model, INDEX_SECOND_PERSON); + Person personToEdit = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new PersonBuilder(personToEdit).withPlatforms().build(); + // removeplatform -> removes the Twitter platform from the second person in unfiltered person list / + // first person in filtered person list + removePlatformCommand.execute(); + undoRedoStack.push(removePlatformCommand); + + // undo -> reverts address book back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, expectedModel); + + expectedModel.updatePerson(personToEdit, editedPerson); + assertNotEquals(personToEdit, model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())); + // redo -> edits the same second person in unfiltered person list and removing the Twitter platform + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void equals() { + RemovePlatformCommand removePlatformFirstCommand = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + RemovePlatformCommand removePlatformSecondCommand = prepareCommand(INDEX_SECOND_PERSON, new HashSet<>()); + + // same object -> returns true + assertTrue(removePlatformFirstCommand.equals(removePlatformFirstCommand)); + + // same values -> returns true + RemovePlatformCommand removePlatformFirstCommandCopy = prepareCommand(INDEX_FIRST_PERSON, new HashSet<>()); + assertTrue(removePlatformFirstCommand.equals(removePlatformFirstCommandCopy)); + + // different types -> returns false + assertFalse(removePlatformFirstCommand.equals(1)); + + // null -> returns false + assertFalse(removePlatformFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(removePlatformFirstCommand.equals(removePlatformSecondCommand)); + } + + /** + * Returns a {@code RemovePlatformCommand} with the parameters {@code index} and {@code platformSet}. + */ + private RemovePlatformCommand prepareCommand(Index index, Set platformSet) { + RemovePlatformCommand removePlatformCommand = new RemovePlatformCommand(index, platformSet); + removePlatformCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + return removePlatformCommand; + } + +} diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java new file mode 100644 index 000000000000..81526a98b5d4 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java @@ -0,0 +1,164 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.prepareRedoCommand; +import static seedu.address.logic.commands.CommandTestUtil.prepareUndoCommand; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.Before; +import org.junit.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.UndoRedoStack; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.testutil.TypicalPersons; + +//@@author Nethergale +/** + * Contains integration tests (interaction with the Model) and unit tests for SortCommand. + */ +public class SortCommandTest { + + private Model model; + private Model customModel; + private Model expectedModel; + private SortCommand sortCommand; + + @Before + public void setUp() throws Exception { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + customModel = new ModelManager(generateModelWithPersons(generatePersonList( + TypicalPersons.JOHN3, TypicalPersons.JOHN2, TypicalPersons.JANE, TypicalPersons.BLAKE, + TypicalPersons.HOB2, TypicalPersons.JOHN1, TypicalPersons.LEONARD, TypicalPersons.HOB1 + )).getAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(generateModelWithPersons(generatePersonList( + TypicalPersons.BLAKE, TypicalPersons.HOB1, TypicalPersons.HOB2, TypicalPersons.JANE, + TypicalPersons.JOHN1, TypicalPersons.JOHN2, TypicalPersons.JOHN3, TypicalPersons.LEONARD + )).getAddressBook(), new UserPrefs()); + sortCommand = new SortCommand(); + sortCommand.setData(model, new CommandHistory(), new UndoRedoStack()); + } + + @Test + public void execute_unfilteredListAlreadySorted_failure() { + assertCommandFailure(sortCommand, model, SortCommand.MESSAGE_FAILURE); + } + + @Test + public void execute_filteredListAlreadySorted_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + assertCommandFailure(sortCommand, model, SortCommand.MESSAGE_FAILURE); + } + + @Test + public void execute_emptyList_failure() { + model.resetData(new AddressBook()); + assertCommandFailure(sortCommand, model, Messages.MESSAGE_ADDRESS_BOOK_EMPTY); + } + + @Test + public void execute_unfilteredListUnsorted_success() { + model.resetData(customModel.getAddressBook()); + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_filteredListUnsorted_success() { + String[] keywords = {"jane", "blake", "hob"}; + Predicate predicate = new NameContainsKeywordsPredicate(Arrays.asList(keywords)); + + model.resetData(customModel.getAddressBook()); + model.updateFilteredPersonList(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void executeUndoRedo_unfilteredList_success() throws Exception { + model.resetData(customModel.getAddressBook()); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + + // sort -> sorts all persons in address book + sortCommand.execute(); + undoRedoStack.push(sortCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, customModel); + + // redo -> sorts all persons in address book again + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + /** + * 1. Sorts all persons from a filtered list, such that the list is kept. + * 2. Undo the sorting. + * 3. The unfiltered list should be shown now. Verify that the list is reverted to before it is sorted. + * 4. Redo the sorting. The list shown should still be unfiltered. + */ + @Test + public void executeUndoRedo_filteredList_success() { + model.resetData(customModel.getAddressBook()); + UndoRedoStack undoRedoStack = new UndoRedoStack(); + UndoCommand undoCommand = prepareUndoCommand(model, undoRedoStack); + RedoCommand redoCommand = prepareRedoCommand(model, undoRedoStack); + + showPersonAtIndex(model, INDEX_THIRD_PERSON); + showPersonAtIndex(expectedModel, Index.fromZeroBased( + expectedModel.getFilteredPersonList().indexOf(model.getFilteredPersonList().get(0)))); + + // sort -> sorts all persons in address book + assertCommandSuccess(sortCommand, model, SortCommand.MESSAGE_SUCCESS, expectedModel); + undoRedoStack.push(sortCommand); + + // undo -> reverts addressbook back to previous state and filtered person list to show all persons + assertCommandSuccess(undoCommand, model, UndoCommand.MESSAGE_SUCCESS, customModel); + + // redo -> sorts all persons in address book again + expectedModel.updateFilteredPersonList(unused -> true); + assertCommandSuccess(redoCommand, model, RedoCommand.MESSAGE_SUCCESS, expectedModel); + } + + //@@author Nethergale-reused + //Reused from https://github.com/nus-cs2103-AY1718S2/addressbook-level3/ + //blob/master/test/java/seedu/addressbook/logic/LogicTest.java with minor modifications + /** + * Creates a list of Persons based on the given Person objects. + */ + private List generatePersonList(Person... persons) { + List personList = new ArrayList<>(); + for (Person p : persons) { + personList.add(p); + } + return personList; + } + + //@@author Nethergale + /** + * Creates a model with all persons found in the list added. + */ + private Model generateModelWithPersons(List personList) throws Exception { + Model m = new ModelManager(); + for (Person p : personList) { + m.addPerson(p); + } + return m; + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddPlatformCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddPlatformCommandParserTest.java new file mode 100644 index 000000000000..cf049cdaece0 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/AddPlatformCommandParserTest.java @@ -0,0 +1,131 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FACEBOOK_LINK_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TWITTER_LINK_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TWITTER_LINK_BOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_THIRD_PERSON; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddPlatformCommand; +import seedu.address.model.smplatform.Link; + +//@@author Nethergale +public class AddPlatformCommandParserTest { + + private static final String FACEBOOK_LINK_FIELD_AMY = " " + PREFIX_LINK + VALID_FACEBOOK_LINK_AMY; + private static final String TWITTER_LINK_FIELD_AMY = " " + PREFIX_LINK + VALID_TWITTER_LINK_AMY; + private static final String TWITTER_LINK_FIELD_BOB = " " + PREFIX_LINK + VALID_TWITTER_LINK_BOB; + + private static final String INVALID_LINK_1 = " " + PREFIX_LINK + "www.google.com"; + private static final String INVALID_LINK_2 = " " + PREFIX_LINK + "www.facebook.com"; + private static final String INVALID_LINK_3 = " " + PREFIX_LINK + "www.twitter.com"; + + private static final String LINK_EMPTY = " " + PREFIX_LINK; + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPlatformCommand.MESSAGE_USAGE); + + private AddPlatformCommandParser parser = new AddPlatformCommandParser(); + + @Test + public void parse_missingParts_failure() { + // no index specified + assertParseFailure(parser, VALID_FACEBOOK_LINK_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", AddPlatformCommand.MESSAGE_LINK_COLLECTION_EMPTY); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + FACEBOOK_LINK_FIELD_AMY, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + FACEBOOK_LINK_FIELD_AMY, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + assertParseFailure(parser, "1" + INVALID_LINK_1, Link.MESSAGE_INVALID_LINK); // unrecognised link + assertParseFailure(parser, "1" + INVALID_LINK_2, Link.MESSAGE_INVALID_LINK); // invalid facebook link + assertParseFailure(parser, "1" + INVALID_LINK_3, Link.MESSAGE_INVALID_LINK); // invalid twitter link + + // valid facebook link followed by invalid facebook link + assertParseFailure(parser, "1" + FACEBOOK_LINK_FIELD_AMY + INVALID_LINK_2, Link.MESSAGE_INVALID_LINK); + // invalid facebook link followed by valid facebook link + assertParseFailure(parser, "1" + INVALID_LINK_2 + FACEBOOK_LINK_FIELD_AMY, Link.MESSAGE_INVALID_LINK); + + // valid twitter link followed by invalid twitter link + assertParseFailure(parser, "1" + TWITTER_LINK_FIELD_AMY + INVALID_LINK_3, Link.MESSAGE_INVALID_LINK); + // invalid twitter link followed by valid twitter link + assertParseFailure(parser, "1" + INVALID_LINK_3 + TWITTER_LINK_FIELD_AMY, Link.MESSAGE_INVALID_LINK); + + //multiple empty link fields + assertParseFailure(parser, "1" + LINK_EMPTY + LINK_EMPTY, Link.MESSAGE_INVALID_LINK); + } + + @Test + public void parse_multipleLinksForSamePlatform_failure() { + assertParseFailure(parser, + "1" + TWITTER_LINK_FIELD_AMY + TWITTER_LINK_FIELD_BOB, Link.MESSAGE_LINK_CONSTRAINTS); + } + + @Test + public void parse_oneLinkFieldSpecified_success() { + Index targetIndex = INDEX_FIRST_PERSON; + String userInput = targetIndex.getOneBased() + FACEBOOK_LINK_FIELD_AMY; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(VALID_FACEBOOK_LINK_AMY), new Link(VALID_FACEBOOK_LINK_AMY)); + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + + userInput = targetIndex.getOneBased() + TWITTER_LINK_FIELD_AMY; + linkMap.clear(); + linkMap.put(Link.getLinkType(VALID_TWITTER_LINK_AMY), new Link(VALID_TWITTER_LINK_AMY)); + expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_multipleLinkFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND_PERSON; + String userInput = targetIndex.getOneBased() + FACEBOOK_LINK_FIELD_AMY + TWITTER_LINK_FIELD_AMY; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(VALID_FACEBOOK_LINK_AMY), new Link(VALID_FACEBOOK_LINK_AMY)); + linkMap.put(Link.getLinkType(VALID_TWITTER_LINK_AMY), new Link(VALID_TWITTER_LINK_AMY)); + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, linkMap); + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_clearPlatforms_success() { + Index targetIndex = INDEX_THIRD_PERSON; + String userInput = targetIndex.getOneBased() + LINK_EMPTY; + + AddPlatformCommand expectedCommand = new AddPlatformCommand(targetIndex, Collections.emptyMap()); + + assertParseSuccess(parser, userInput, expectedCommand); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 7466da232666..f2f346b1ea3e 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -8,7 +8,11 @@ import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.junit.Rule; @@ -16,6 +20,7 @@ import org.junit.rules.ExpectedException; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddPlatformCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; @@ -25,12 +30,20 @@ import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoginCommand; import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.RemovePlatformCommand; +import seedu.address.logic.commands.SearchCommand; import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.SortCommand; import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.account.Account; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.smplatform.Link; +import seedu.address.testutil.AccountBuilder; +import seedu.address.testutil.AccountUtil; import seedu.address.testutil.EditPersonDescriptorBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -41,11 +54,27 @@ public class AddressBookParserTest { private final AddressBookParser parser = new AddressBookParser(); + //@@author shadow2496 + @Test + public void parseCommand_login() throws Exception { + Account account = new AccountBuilder().build(); + LoginCommand command = (LoginCommand) parser.parseCommand( + LoginCommand.COMMAND_WORD + " " + AccountUtil.getAccountDetails(account)); + assertEquals(new LoginCommand(account), command); + } + + //@@author + @Test public void parseCommand_add() throws Exception { Person person = new PersonBuilder().build(); AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); assertEquals(new AddCommand(person), command); + + //@@author Nethergale + AddCommand commandAlias = (AddCommand) parser.parseCommand(PersonUtil.getAddCommandAlias(person)); + assertEquals(new AddCommand(person), commandAlias); + //@@author } @Test @@ -109,6 +138,45 @@ public void parseCommand_list() throws Exception { assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + //@@author Nethergale + @Test + public void parseCommand_sort() throws Exception { + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD) instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " 3") instanceof SortCommand); + } + + @Test + public void parseCommand_addPlatform() throws Exception { + String testLink = "www.facebook.com/example"; + Map linkMap = new HashMap<>(); + linkMap.put(Link.getLinkType(testLink), new Link(testLink)); + AddPlatformCommand command = (AddPlatformCommand) parser.parseCommand(AddPlatformCommand.COMMAND_WORD + + " " + INDEX_FIRST_PERSON.getOneBased() + + " " + CliSyntax.PREFIX_LINK + testLink); + + assertEquals(new AddPlatformCommand(INDEX_FIRST_PERSON, linkMap), command); + } + + @Test + public void parseCommand_removePlatform() throws Exception { + String platform = "facebook"; + Set platformSet = new HashSet<>(); + platformSet.add(platform); + RemovePlatformCommand command = (RemovePlatformCommand) parser.parseCommand(RemovePlatformCommand.COMMAND_WORD + + " " + INDEX_FIRST_PERSON.getOneBased() + + " " + CliSyntax.PREFIX_SOCIAL_MEDIA_PLATFORM + platform); + + assertEquals(new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet), command); + } + + @Test + public void parseCommand_search() throws Exception { + String searchName = "foo"; + SearchCommand command = (SearchCommand) parser.parseCommand( + SearchCommand.COMMAND_WORD + " " + searchName); + assertEquals(new SearchCommand("all", searchName), command); + } + //@@author @Test public void parseCommand_select() throws Exception { SelectCommand command = (SelectCommand) parser.parseCommand( diff --git a/src/test/java/seedu/address/logic/parser/FindWithTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindWithTagCommandParserTest.java new file mode 100644 index 000000000000..3774becf2fb9 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindWithTagCommandParserTest.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; + +import org.junit.Test; + +import seedu.address.logic.commands.FindWithTagCommand; +import seedu.address.model.person.TagContainsKeywordsPredicate; + +//@@author KevinChuangCH +public class FindWithTagCommandParserTest { + + private FindWithTagCommandParser parser = new FindWithTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FindWithTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindWithTagCommand() { + // no leading and trailing whitespaces + FindWithTagCommand expectedFindWithTagCommand = + new FindWithTagCommand(new TagContainsKeywordsPredicate(Arrays.asList("neighbour", "owesMoney"))); + assertParseSuccess(parser, "neighbour owesMoney", expectedFindWithTagCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n neighbour \n \t owesMoney \t", expectedFindWithTagCommand); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/LoginCommandParserTest.java b/src/test/java/seedu/address/logic/parser/LoginCommandParserTest.java new file mode 100644 index 000000000000..035c5fba0ce0 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/LoginCommandParserTest.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.commands.CommandTestUtil.INVALID_PASSWORD_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_USERNAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.PASSWORD_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.PASSWORD_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY; +import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; +import static seedu.address.logic.commands.CommandTestUtil.USERNAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.USERNAME_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PASSWORD_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_USERNAME_BOB; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.Test; + +import seedu.address.logic.commands.LoginCommand; +import seedu.address.model.account.Account; +import seedu.address.model.account.Password; +import seedu.address.model.account.Username; +import seedu.address.testutil.AccountBuilder; + +//@@author shadow2496 +public class LoginCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE); + + private LoginCommandParser parser = new LoginCommandParser(); + + @Test + public void parse_allFieldsPresent_success() { + Account account = new AccountBuilder().withUsername(VALID_USERNAME_BOB) + .withPassword(VALID_PASSWORD_BOB).build(); + LoginCommand expectedCommand = new LoginCommand(account); + + // whitespace only preamble + assertParseSuccess(parser, PREAMBLE_WHITESPACE + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + expectedCommand); + + // multiple usernames - last username accepted + assertParseSuccess(parser, USERNAME_DESC_AMY + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + expectedCommand); + + // multiple passwords - last password accepted + assertParseSuccess(parser, USERNAME_DESC_BOB + PASSWORD_DESC_AMY + PASSWORD_DESC_BOB, + expectedCommand); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + // missing username prefix + assertParseFailure(parser, VALID_USERNAME_BOB + PASSWORD_DESC_BOB, MESSAGE_INVALID_FORMAT); + + // missing password prefix + assertParseFailure(parser, USERNAME_DESC_BOB + VALID_PASSWORD_BOB, MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + // invalid username + assertParseFailure(parser, INVALID_USERNAME_DESC + PASSWORD_DESC_BOB, + Username.MESSAGE_USERNAME_CONSTRAINTS); + + // invalid password + assertParseFailure(parser, USERNAME_DESC_BOB + INVALID_PASSWORD_DESC, + Password.MESSAGE_PASSWORD_CONSTRAINTS); + + // multiple invalid values, but only the first invalid value is captured + assertParseFailure(parser, INVALID_USERNAME_DESC + INVALID_PASSWORD_DESC, + Username.MESSAGE_USERNAME_CONSTRAINTS); + } + + @Test + public void parse_invalidPreamble_failure() { + // non-empty preamble + assertParseFailure(parser, PREAMBLE_NON_EMPTY + USERNAME_DESC_BOB + PASSWORD_DESC_BOB, + MESSAGE_INVALID_FORMAT); + } +} diff --git a/src/test/java/seedu/address/logic/parser/RemovePlatformCommandParserTest.java b/src/test/java/seedu/address/logic/parser/RemovePlatformCommandParserTest.java new file mode 100644 index 000000000000..7faea1e65497 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/RemovePlatformCommandParserTest.java @@ -0,0 +1,44 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SOCIAL_MEDIA_PLATFORM; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import seedu.address.logic.commands.RemovePlatformCommand; +import seedu.address.model.smplatform.Link; + +//@@author Nethergale +public class RemovePlatformCommandParserTest { + + private static final String FACEBOOK_PLATFORM_FIELD = " " + PREFIX_SOCIAL_MEDIA_PLATFORM + Link.FACEBOOK_LINK_TYPE; + private static final String TWITTER_PLATFORM_FIELD = " " + PREFIX_SOCIAL_MEDIA_PLATFORM + Link.TWITTER_LINK_TYPE; + + private RemovePlatformCommandParser parser = new RemovePlatformCommandParser(); + private Set platformSet = new HashSet<>(); + + @Test + public void parse_validArgsNoPlatformFieldsSpecified_returnsRemovePlatformCommand() { + assertParseSuccess(parser, "1", new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet)); + } + + @Test + public void parse_validArgsPlatformFieldsSpecified_returnsRemovePlatformCommand() { + platformSet.add(Link.FACEBOOK_LINK_TYPE); + platformSet.add(Link.TWITTER_LINK_TYPE); + assertParseSuccess(parser, "1" + FACEBOOK_PLATFORM_FIELD + TWITTER_PLATFORM_FIELD, + new RemovePlatformCommand(INDEX_FIRST_PERSON, platformSet)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "a", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemovePlatformCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/SearchCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SearchCommandParserTest.java new file mode 100644 index 000000000000..12b08833cd76 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SearchCommandParserTest.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.Test; + +import seedu.address.logic.commands.SearchCommand; + +//@@author Nethergale +public class SearchCommandParserTest { + + private static final String INVALID_SEARCH_NAME = "a%b2$c"; + private static final String VALID_SEARCH_NAME = "abc"; + private static final String TWITTER_PLATFORM = "twitter"; + private static final String FACEBOOK_PLATFORM_ALIAS = "fb"; + + private SearchCommandParser parser = new SearchCommandParser(); + + @Test + public void parse_noPlatformSpecifiedInvalidSearchName_failure() { + assertParseFailure(parser, INVALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_noPlatformSpecifiedValidSearchName_success() { + assertParseSuccess(parser, VALID_SEARCH_NAME, new SearchCommand("all", VALID_SEARCH_NAME)); + } + + @Test + public void parse_validPlatformSpecifiedInvalidSearchName_failure() { + assertParseFailure(parser, TWITTER_PLATFORM + ", " + INVALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validPlatformSpecifiedValidSearchName_success() { + assertParseSuccess(parser, FACEBOOK_PLATFORM_ALIAS + ", " + VALID_SEARCH_NAME, + new SearchCommand(FACEBOOK_PLATFORM_ALIAS, VALID_SEARCH_NAME)); + } + + @Test + public void parse_invalidPlatformSpecifiedValidSearchName_failure() { + String invalidPlatform = "aha"; + assertParseFailure(parser, invalidPlatform + ", " + VALID_SEARCH_NAME, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + assertParseFailure(parser, "yo, test, this, command", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/model/account/PasswordTest.java b/src/test/java/seedu/address/model/account/PasswordTest.java new file mode 100644 index 000000000000..95b6b7c56a26 --- /dev/null +++ b/src/test/java/seedu/address/model/account/PasswordTest.java @@ -0,0 +1,38 @@ +package seedu.address.model.account; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import seedu.address.testutil.Assert; + +//@@author shadow2496 +public class PasswordTest { + + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Password(null)); + } + + @Test + public void constructor_invalidPassword_throwsIllegalArgumentException() { + String invalidPassword = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Password(invalidPassword)); + } + + @Test + public void isValidPassword() { + // null password + Assert.assertThrows(NullPointerException.class, () -> Password.isValidPassword(null)); + + // invalid passwords + assertFalse(Password.isValidPassword("")); // empty string + assertFalse(Password.isValidPassword(" ")); // spaces only + + // valid passwords + assertTrue(Password.isValidPassword("amy1111")); + assertTrue(Password.isValidPassword("-")); // one character + assertTrue(Password.isValidPassword("amy1111!very!very!very!long!example")); // long password + } +} diff --git a/src/test/java/seedu/address/model/account/UsernameTest.java b/src/test/java/seedu/address/model/account/UsernameTest.java new file mode 100644 index 000000000000..d0a73a92d46e --- /dev/null +++ b/src/test/java/seedu/address/model/account/UsernameTest.java @@ -0,0 +1,38 @@ +package seedu.address.model.account; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import seedu.address.testutil.Assert; + +//@@author shadow2496 +public class UsernameTest { + + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Username(null)); + } + + @Test + public void constructor_invalidUsername_throwsIllegalArgumentException() { + String invalidUsername = ""; + Assert.assertThrows(IllegalArgumentException.class, () -> new Username(invalidUsername)); + } + + @Test + public void isValidUsername() { + // null username + Assert.assertThrows(NullPointerException.class, () -> Username.isValidUsername(null)); + + // invalid usernames + assertFalse(Username.isValidUsername("")); // empty string + assertFalse(Username.isValidUsername(" ")); // spaces only + + // valid usernames + assertTrue(Username.isValidUsername("amybee")); + assertTrue(Username.isValidUsername("-")); // one character + assertTrue(Username.isValidUsername("amybee@very-very-very-long-example.com")); // long username + } +} diff --git a/src/test/java/seedu/address/model/smplatform/LinkTest.java b/src/test/java/seedu/address/model/smplatform/LinkTest.java new file mode 100644 index 000000000000..6515cca34813 --- /dev/null +++ b/src/test/java/seedu/address/model/smplatform/LinkTest.java @@ -0,0 +1,44 @@ +package seedu.address.model.smplatform; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import seedu.address.testutil.Assert; + +//@@author Nethergale +public class LinkTest { + @Test + public void constructor_null_throwsNullPointerException() { + Assert.assertThrows(NullPointerException.class, () -> new Link(null)); + } + + @Test + public void isValidLink() { + // null link + Assert.assertThrows(NullPointerException.class, () -> Link.isValidLink(null)); + + // invalid links + assertFalse(Link.isValidLink("")); // empty string + assertFalse(Link.isValidLink(" ")); // spaces only + assertFalse(Link.isValidLink("www.google.com/")); // unknown link + assertFalse(Link.isValidLink("www.facebook.com/")); // facebook link without any path specified + assertFalse(Link.isValidLink("www.facebook.com////")); // facebook link with slashes only + assertFalse(Link.isValidLink("www.facebook.com/ /")); // facebook link with space as profile username + assertFalse(Link.isValidLink("www.twitter.com/")); // twitter link without any path specified + assertFalse(Link.isValidLink("www.twitter.com////")); // twitter link with slashes only + assertFalse(Link.isValidLink("www.twitter.com/ /")); // twitter link with space as username handle + + // valid links + assertTrue(Link.isValidLink("https://www.facebook.com/abc")); // facebook page with protocol and subdomain + assertTrue(Link.isValidLink("http://facebook.com/abc")); // facebook page with protocol only + assertTrue(Link.isValidLink("www.facebook.com/teo.yong")); // facebook page with subdomain only + assertTrue(Link.isValidLink("facebook.com/abc")); // facebook page with profile username only + assertTrue(Link.isValidLink("facebook.com/profile.php?id=100008354955053")); // facebook page with ID + assertTrue(Link.isValidLink("https://www.twitter.com/abc")); // twitter page with protocol and subdomain + assertTrue(Link.isValidLink("http://twitter.com/__ChrisLee")); // twitter page with protocol only + assertTrue(Link.isValidLink("www.twitter.com/yosp")); // twitter page with subdomain only + assertTrue(Link.isValidLink("twitter.com/abc")); // twitter page with username handle only + } +} diff --git a/src/test/java/seedu/address/storage/XmlAdaptedPersonTest.java b/src/test/java/seedu/address/storage/XmlAdaptedPersonTest.java index c3c91a5c27a7..ffade35bc87c 100644 --- a/src/test/java/seedu/address/storage/XmlAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/XmlAdaptedPersonTest.java @@ -15,6 +15,7 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.smplatform.Link; import seedu.address.testutil.Assert; public class XmlAdaptedPersonTest { @@ -22,16 +23,27 @@ public class XmlAdaptedPersonTest { private static final String INVALID_PHONE = "+651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; + private static final String INVALID_SOCIAL_MEDIA_PLATFORM_LINK = "www.google.com"; private static final String INVALID_TAG = "#friend"; private static final String VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); private static final String VALID_EMAIL = BENSON.getEmail().toString(); private static final String VALID_ADDRESS = BENSON.getAddress().toString(); + private static final List VALID_PLATFORMS = getXmlAdaptedSocialMediaPlatforms(); private static final List VALID_TAGS = BENSON.getTags().stream() .map(XmlAdaptedTag::new) .collect(Collectors.toList()); + private static List getXmlAdaptedSocialMediaPlatforms() { + List list = new ArrayList<>(); + for (String key : BENSON.getSocialMediaPlatformMap().keySet()) { + list.add(new XmlAdaptedSocialMediaPlatform(key, + BENSON.getSocialMediaPlatformMap().get(key).getLink().value)); + } + return list; + } + @Test public void toModelType_validPersonDetails_returnsPerson() throws Exception { XmlAdaptedPerson person = new XmlAdaptedPerson(BENSON); @@ -40,70 +52,84 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { - XmlAdaptedPerson person = - new XmlAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = Name.MESSAGE_NAME_CONSTRAINTS; Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - XmlAdaptedPerson person = new XmlAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_invalidPhone_throwsIllegalValueException() { - XmlAdaptedPerson person = - new XmlAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = Phone.MESSAGE_PHONE_CONSTRAINTS; Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, null, VALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_invalidEmail_throwsIllegalValueException() { - XmlAdaptedPerson person = - new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = Email.MESSAGE_EMAIL_CONSTRAINTS; Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, null, + VALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_invalidAddress_throwsIllegalValueException() { - XmlAdaptedPerson person = - new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + INVALID_ADDRESS, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = Address.MESSAGE_ADDRESS_CONSTRAINTS; Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + null, VALID_PLATFORMS, VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } + @Test + public void toModelType_invalidSocialMediaPlatforms_throwsIllegalValueException() { + List invalidSocialMediaPlatforms = new ArrayList<>(VALID_PLATFORMS); + invalidSocialMediaPlatforms.add(new XmlAdaptedSocialMediaPlatform( + Link.getLinkType(INVALID_SOCIAL_MEDIA_PLATFORM_LINK), INVALID_SOCIAL_MEDIA_PLATFORM_LINK)); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, invalidSocialMediaPlatforms, VALID_TAGS); + Assert.assertThrows(IllegalValueException.class, person::toModelType); + } + @Test public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new XmlAdaptedTag(INVALID_TAG)); - XmlAdaptedPerson person = - new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + XmlAdaptedPerson person = new XmlAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_PLATFORMS, invalidTags); Assert.assertThrows(IllegalValueException.class, person::toModelType); } diff --git a/src/test/java/seedu/address/testutil/AccountBuilder.java b/src/test/java/seedu/address/testutil/AccountBuilder.java new file mode 100644 index 000000000000..9172b761aaa5 --- /dev/null +++ b/src/test/java/seedu/address/testutil/AccountBuilder.java @@ -0,0 +1,43 @@ +package seedu.address.testutil; + +import seedu.address.model.account.Account; +import seedu.address.model.account.Password; +import seedu.address.model.account.Username; + +//@@author shadow2496 +/** + * A utility class to help with building Account objects. + */ +public class AccountBuilder { + + private static final String DEFAULT_USERNAME = "alicepauline"; + private static final String DEFAULT_PASSWORD = "alice8535"; + + private Username username; + private Password password; + + public AccountBuilder() { + username = new Username(DEFAULT_USERNAME); + password = new Password(DEFAULT_PASSWORD); + } + + /** + * Sets the {@code Username} of the {@code Account} that we are building. + */ + public AccountBuilder withUsername(String username) { + this.username = new Username(username); + return this; + } + + /** + * Sets the {@code Password} of the {@code Account} that we are building. + */ + public AccountBuilder withPassword(String password) { + this.password = new Password(password); + return this; + } + + public Account build() { + return new Account(username, password); + } +} diff --git a/src/test/java/seedu/address/testutil/AccountUtil.java b/src/test/java/seedu/address/testutil/AccountUtil.java new file mode 100644 index 000000000000..35b7eab55b11 --- /dev/null +++ b/src/test/java/seedu/address/testutil/AccountUtil.java @@ -0,0 +1,21 @@ +package seedu.address.testutil; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_PASSWORD; +import static seedu.address.logic.parser.CliSyntax.PREFIX_USERNAME; + +import seedu.address.model.account.Account; + +//@@author shadow2496 +/** + * A utility class for Account. + */ +public class AccountUtil { + + /** + * Returns the part of command string for the given {@code account}'s details. + */ + public static String getAccountDetails(Account account) { + return PREFIX_USERNAME + account.getUsername().value + " " + + PREFIX_PASSWORD + account.getPassword().value; + } +} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index b124fc1d73b1..b8101cbb84b9 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -1,6 +1,8 @@ package seedu.address.testutil; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import seedu.address.model.person.Address; @@ -8,6 +10,7 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.model.tag.Tag; import seedu.address.model.util.SampleDataUtil; @@ -16,16 +19,18 @@ */ public class PersonBuilder { - public static final String DEFAULT_NAME = "Alice Pauline"; - public static final String DEFAULT_PHONE = "85355255"; - public static final String DEFAULT_EMAIL = "alice@gmail.com"; - public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; - public static final String DEFAULT_TAGS = "friends"; + private static final String DEFAULT_NAME = "Alice Pauline"; + private static final String DEFAULT_PHONE = "85355255"; + private static final String DEFAULT_EMAIL = "alice@gmail.com"; + private static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; + private static final String DEFAULT_SOCIAL_MEDIA_PLATFORM_LINK = ""; + private static final String DEFAULT_TAGS = "friends"; private Name name; private Phone phone; private Email email; private Address address; + private Map smpMap; private Set tags; public PersonBuilder() { @@ -33,6 +38,7 @@ public PersonBuilder() { phone = new Phone(DEFAULT_PHONE); email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); + smpMap = SampleDataUtil.getSocialMediaPlatformMap(DEFAULT_SOCIAL_MEDIA_PLATFORM_LINK); tags = SampleDataUtil.getTagSet(DEFAULT_TAGS); } @@ -44,6 +50,7 @@ public PersonBuilder(Person personToCopy) { phone = personToCopy.getPhone(); email = personToCopy.getEmail(); address = personToCopy.getAddress(); + smpMap = new HashMap<>(personToCopy.getSocialMediaPlatformMap()); tags = new HashSet<>(personToCopy.getTags()); } @@ -55,6 +62,15 @@ public PersonBuilder withName(String name) { return this; } + /** + * Parses the {@code links} into social media platforms, add them into a {@code Map} + * and set the map to the {@code Person} that we are building. + */ + public PersonBuilder withPlatforms(String ... links) { + smpMap = SampleDataUtil.getSocialMediaPlatformMap(links); + return this; + } + /** * Parses the {@code tags} into a {@code Set} and set it to the {@code Person} that we are building. */ @@ -88,7 +104,7 @@ public PersonBuilder withEmail(String email) { } public Person build() { - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, smpMap, tags); } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 642d4f174514..64d00a09deac 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -21,6 +21,15 @@ public static String getAddCommand(Person person) { return AddCommand.COMMAND_WORD + " " + getPersonDetails(person); } + //@@author Nethergale + /** + * Returns an add command alias string for adding the {@code person}. + */ + public static String getAddCommandAlias(Person person) { + return AddCommand.COMMAND_ALIAS + " " + getPersonDetails(person); + } + + //@@author /** * Returns the part of command string for the given {@code person}'s details. */ diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index 6d7bdbfc55ed..22e2f6c2f8d5 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -26,28 +26,46 @@ public class TypicalPersons { public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("85355255") - .withTags("friends").build(); + .withPhone("85355255").withPlatforms("www.facebook.com/alice.pauline") + .withTags("neighbour", "friends").build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") - .withAddress("311, Clementi Ave 2, #02-25") - .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); + .withAddress("311, Clementi Ave 2, #02-25").withEmail("johnd@example.com") + .withPhone("98765432").withPlatforms("www.twitter.com/bend") + .withTags("armyBuddy", "owesMoney", "friends").build(); public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); + .withEmail("heinz@example.com").withAddress("wall street").withTags("classmate", "PC2103").build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").build(); + .withEmail("cornelia@example.com").withAddress("10th street") + .withTags("armyBuddy", "friends", "roommate").build(); public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); + .withEmail("werner@example.com").withAddress("michegan ave").withTags("professor", "PC3196").build(); public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); + .withEmail("lydia@example.com").withAddress("little tokyo").withTags("labPartner").build(); public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withEmail("anna@example.com").withAddress("4th street").withTags("teammate").build(); // Manually added public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") .withEmail("stefan@example.com").withAddress("little india").build(); public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") .withEmail("hans@example.com").withAddress("chicago ave").build(); + public static final Person BLAKE = new PersonBuilder().withName("Blake Stu").withPhone("9390845") + .withEmail("blakestu@example.com").withAddress("Trade Route").build(); + public static final Person HOB1 = new PersonBuilder().withName("Hob Wright").withPhone("8406714") + .withEmail("hob1@example.com").withAddress("Grey Crane").build(); + public static final Person HOB2 = new PersonBuilder().withName("Hob wright").withPhone("8024786") + .withEmail("hob2@example.com").withAddress("Hype Street").build(); + public static final Person JANE = new PersonBuilder().withName("Jane Low").withPhone("8390841") + .withEmail("janelow@example.com").withAddress("Neverend").build(); + public static final Person JOHN1 = new PersonBuilder().withName("JohN Doe").withPhone("9836241") + .withEmail("john1@example.com").withAddress("Holly Way").build(); + public static final Person JOHN2 = new PersonBuilder().withName("John Doe").withPhone("9816482") + .withEmail("john2@example.com").withAddress("Peace Street").build(); + public static final Person JOHN3 = new PersonBuilder().withName("John doe").withPhone("8373364") + .withEmail("john3@example.com").withAddress("Lace Walk").build(); + public static final Person LEONARD = new PersonBuilder().withName("Leonard Xu").withPhone("9067234") + .withEmail("leonard@example.com").withAddress("Daze Lane").build(); + // Manually added - Person's details found in {@code CommandTestUtil} public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) @@ -56,6 +74,7 @@ public class TypicalPersons { .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) .build(); + public static final String KEYWORD_MATCHING_ARMYBUDDY = "armyBuddy"; // A keyword that matches FRIENDS public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER private TypicalPersons() {} // prevents instantiation diff --git a/src/test/java/seedu/address/ui/BrowserPanelTest.java b/src/test/java/seedu/address/ui/BrowserPanelTest.java index 48aab940f8a8..ba485c9e608d 100644 --- a/src/test/java/seedu/address/ui/BrowserPanelTest.java +++ b/src/test/java/seedu/address/ui/BrowserPanelTest.java @@ -15,6 +15,11 @@ import guitests.guihandles.BrowserPanelHandle; import seedu.address.MainApp; import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; +import seedu.address.logic.Logic; +import seedu.address.logic.LogicManager; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.smplatform.Link; public class BrowserPanelTest extends GuiUnitTest { private PersonPanelSelectionChangedEvent selectionChangedEventStub; @@ -24,9 +29,12 @@ public class BrowserPanelTest extends GuiUnitTest { @Before public void setUp() { + Model model = new ModelManager(); + Logic logic = new LogicManager(model); + selectionChangedEventStub = new PersonPanelSelectionChangedEvent(new PersonCard(ALICE, 0)); - guiRobot.interact(() -> browserPanel = new BrowserPanel()); + guiRobot.interact(() -> browserPanel = new BrowserPanel(logic)); uiPartRule.setUiPart(browserPanel); browserPanelHandle = new BrowserPanelHandle(browserPanel.getRoot()); @@ -40,7 +48,9 @@ public void display() throws Exception { // associated web page of a person postNow(selectionChangedEventStub); - URL expectedPersonUrl = new URL(BrowserPanel.SEARCH_PAGE_URL + ALICE.getName().fullName.replaceAll(" ", "%20")); + String storedLink = ALICE.getSocialMediaPlatformMap().get(Link.FACEBOOK_LINK_TYPE).getLink().value; + URL expectedPersonUrl = new URL("https://m." + + storedLink.substring(storedLink.indexOf(Link.FACEBOOK_LINK_TYPE))); waitUntilBrowserLoaded(browserPanelHandle); assertEquals(expectedPersonUrl, browserPanelHandle.getLoadedUrl()); diff --git a/src/test/java/seedu/address/ui/PersonCardTest.java b/src/test/java/seedu/address/ui/PersonCardTest.java index 42f840faa3b4..32b11f9caca0 100644 --- a/src/test/java/seedu/address/ui/PersonCardTest.java +++ b/src/test/java/seedu/address/ui/PersonCardTest.java @@ -5,10 +5,23 @@ import static org.junit.Assert.assertTrue; import static seedu.address.ui.testutil.GuiTestAssert.assertCardDisplaysPerson; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + import org.junit.Test; import guitests.guihandles.PersonCardHandle; +import seedu.address.model.person.Address; +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.smplatform.Facebook; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; +import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; public class PersonCardTest extends GuiUnitTest { @@ -26,6 +39,29 @@ public void display() { personCard = new PersonCard(personWithTags, 2); uiPartRule.setUiPart(personCard); assertCardDisplay(personCard, personWithTags, 2); + + //@@author Nethergale + // with platforms + String[] links = {"www.facebook.com/examplepage", "www.twitter.com/examplepage"}; + Person personWithPlatforms = new PersonBuilder().withPlatforms(links).build(); + personCard = new PersonCard(personWithPlatforms, 3); + uiPartRule.setUiPart(personCard); + assertCardDisplay(personCard, personWithPlatforms, 3); + + // with platforms purposely put into wrong key, should not display any icons + Map customSmpMap = new HashMap(); + customSmpMap.put(Link.UNKNOWN_LINK_TYPE, new Facebook(new Link("www.facebook.com/testlink"))); + Set defaultTags = new HashSet<>(); + defaultTags.add(new Tag("friends")); + + Person personWithIncorrectSmpMap = new Person( + new Name("Alice Pauline"), new Phone("85355255"), + new Email("alice@gmail.com"), new Address("123, Jurong West Ave 6, #08-111"), + customSmpMap, defaultTags); + personCard = new PersonCard(personWithIncorrectSmpMap, 4); + uiPartRule.setUiPart(personCard); + assertCardDisplay(personCard, personWithIncorrectSmpMap, 4); + //@@author } @Test diff --git a/src/test/java/seedu/address/ui/ResultDisplayTest.java b/src/test/java/seedu/address/ui/ResultDisplayTest.java index acea62615ff4..7f62e5c1bb89 100644 --- a/src/test/java/seedu/address/ui/ResultDisplayTest.java +++ b/src/test/java/seedu/address/ui/ResultDisplayTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static seedu.address.testutil.EventsUtil.postNow; +import java.util.ArrayList; + import org.junit.Before; import org.junit.Test; @@ -11,17 +13,33 @@ public class ResultDisplayTest extends GuiUnitTest { - private static final NewResultAvailableEvent NEW_RESULT_EVENT_STUB = new NewResultAvailableEvent("Stub"); + //@@author shadow2496 + private static final NewResultAvailableEvent NEW_RESULT_EVENT_NON_ERROR = + new NewResultAvailableEvent("Non Error", false); + private static final NewResultAvailableEvent NEW_RESULT_EVENT_ERROR = + new NewResultAvailableEvent("Error", true); + + private ArrayList defaultStyleOfResultDisplay; + private ArrayList errorStyleOfResultDisplay; + + //@@author private ResultDisplayHandle resultDisplayHandle; @Before public void setUp() { ResultDisplay resultDisplay = new ResultDisplay(); - uiPartRule.setUiPart(resultDisplay); - resultDisplayHandle = new ResultDisplayHandle(getChildNode(resultDisplay.getRoot(), ResultDisplayHandle.RESULT_DISPLAY_ID)); + //@@author shadow2496 + uiPartRule.setUiPart(resultDisplay); + + defaultStyleOfResultDisplay = new ArrayList<>(resultDisplayHandle.getStyleClass()); + + errorStyleOfResultDisplay = new ArrayList<>(defaultStyleOfResultDisplay); + errorStyleOfResultDisplay.add(ResultDisplay.ERROR_STYLE_CLASS); + + //@@author } @Test @@ -29,10 +47,40 @@ public void display() { // default result text guiRobot.pauseForHuman(); assertEquals("", resultDisplayHandle.getText()); + //@@author shadow2496 + assertEquals(defaultStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); + + // new error result received + postNow(NEW_RESULT_EVENT_ERROR); + guiRobot.pauseForHuman(); + assertBehaviorForErrorResult(); - // new result received - postNow(NEW_RESULT_EVENT_STUB); + // new non-error result received + postNow(NEW_RESULT_EVENT_NON_ERROR); guiRobot.pauseForHuman(); - assertEquals(NEW_RESULT_EVENT_STUB.message, resultDisplayHandle.getText()); + assertBehaviorForNonErrorResult(); + + //@@author + } + + //@@author shadow2496 + /** + * Verifies a result which has an error that
+ * - the text remains
+ * - the result display's style is the same as {@code errorStyleOfResultDisplay}. + */ + private void assertBehaviorForErrorResult() { + assertEquals(NEW_RESULT_EVENT_ERROR.message, resultDisplayHandle.getText()); + assertEquals(errorStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); + } + + /** + * Verifies a result which doesn't have an error that
+ * - the text remains
+ * - the result display's style is the same as {@code defaultStyleOfResultDisplay}. + */ + private void assertBehaviorForNonErrorResult() { + assertEquals(NEW_RESULT_EVENT_NON_ERROR.message, resultDisplayHandle.getText()); + assertEquals(defaultStyleOfResultDisplay, resultDisplayHandle.getStyleClass()); } } diff --git a/src/test/java/systemtests/AddCommandSystemTest.java b/src/test/java/systemtests/AddCommandSystemTest.java index 3254b60154c4..b8a22553cf22 100644 --- a/src/test/java/systemtests/AddCommandSystemTest.java +++ b/src/test/java/systemtests/AddCommandSystemTest.java @@ -235,6 +235,7 @@ private void assertCommandSuccess(String command, Model expectedModel, String ex assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertStatusBarUnchangedExceptSyncStatus(); } @@ -256,6 +257,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } } diff --git a/src/test/java/systemtests/AddressBookSystemTest.java b/src/test/java/systemtests/AddressBookSystemTest.java index 97cdf96d65b8..0685c3983f90 100644 --- a/src/test/java/systemtests/AddressBookSystemTest.java +++ b/src/test/java/systemtests/AddressBookSystemTest.java @@ -12,9 +12,11 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import org.junit.After; import org.junit.Before; @@ -38,9 +40,11 @@ import seedu.address.logic.commands.SelectCommand; import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.smplatform.Link; +import seedu.address.model.smplatform.SocialMediaPlatform; import seedu.address.testutil.TypicalPersons; -import seedu.address.ui.BrowserPanel; import seedu.address.ui.CommandBox; +import seedu.address.ui.ResultDisplay; /** * A system test class for AddressBook, which provides access to handles of GUI components and helper methods @@ -53,6 +57,10 @@ public abstract class AddressBookSystemTest { private static final List COMMAND_BOX_DEFAULT_STYLE = Arrays.asList("text-input", "text-field"); private static final List COMMAND_BOX_ERROR_STYLE = Arrays.asList("text-input", "text-field", CommandBox.ERROR_STYLE_CLASS); + private static final List RESULT_DISPLAY_DEFAULT_STYLE = + Arrays.asList("text-input", "text-area", "result-display"); + private static final List RESULT_DISPLAY_ERROR_STYLE = + Arrays.asList("text-input", "text-area", "result-display", ResultDisplay.ERROR_STYLE_CLASS); private MainWindowHandle mainWindowHandle; private TestApp testApp; @@ -204,6 +212,7 @@ protected void assertSelectedCardDeselected() { assertFalse(getPersonListPanel().isAnyCardSelected()); } + //@@author Nethergale /** * Asserts that the browser's url is changed to display the details of the person in the person list panel at * {@code expectedSelectedCardIndex}, and only the card at {@code expectedSelectedCardIndex} is selected. @@ -211,18 +220,29 @@ protected void assertSelectedCardDeselected() { * @see PersonListPanelHandle#isSelectedPersonCardChanged() */ protected void assertSelectedCardChanged(Index expectedSelectedCardIndex) { - String selectedCardName = getPersonListPanel().getHandleToSelectedCard().getName(); + String selectedBrowserLink = ""; + Map selectedPersonSmpMap = getModel().getFilteredPersonList().get( + expectedSelectedCardIndex.getZeroBased()).getSocialMediaPlatformMap(); + List keyList = new ArrayList<>(selectedPersonSmpMap.keySet()); + if (!keyList.isEmpty()) { + selectedBrowserLink = selectedPersonSmpMap.get(keyList.get(0)).getLink().value; + } + URL expectedUrl; + URL actualUrl; + try { - expectedUrl = new URL(BrowserPanel.SEARCH_PAGE_URL + selectedCardName.replaceAll(" ", "%20")); + expectedUrl = getExpectedUrl(selectedBrowserLink); + actualUrl = getBrowserPanel().getLoadedUrl(Link.getLinkType(selectedBrowserLink)); } catch (MalformedURLException mue) { throw new AssertionError("URL expected to be valid."); } - assertEquals(expectedUrl, getBrowserPanel().getLoadedUrl()); + assertEquals(expectedUrl, actualUrl); assertEquals(expectedSelectedCardIndex.getZeroBased(), getPersonListPanel().getSelectedCardIndex()); } + //@@author /** * Asserts that the browser's url and the selected card in the person list panel remain unchanged. * @see BrowserPanelHandle#isUrlChanged() @@ -247,6 +267,23 @@ protected void assertCommandBoxShowsErrorStyle() { assertEquals(COMMAND_BOX_ERROR_STYLE, getCommandBox().getStyleClass()); } + //@@author shadow2496 + /** + * Asserts that the result display's shows the default style. + */ + protected void assertResultDisplayShowsDefaultStyle() { + assertEquals(RESULT_DISPLAY_DEFAULT_STYLE, getResultDisplay().getStyleClass()); + } + + /** + * Asserts that the result display's shows the error style. + */ + protected void assertResultDisplayShowsErrorStyle() { + assertEquals(RESULT_DISPLAY_ERROR_STYLE, getResultDisplay().getStyleClass()); + } + + //@@author + /** * Asserts that the entire status bar remains the same. */ @@ -284,6 +321,22 @@ private void assertApplicationStartingStateIsCorrect() { } } + //@@author Nethergale + /** + * Returns the expected URL in the correct format when provided with a String type {@code url}. + * {@code personName} is utilised when no URLs of the available platforms can be constructed. + */ + protected URL getExpectedUrl(String url) throws MalformedURLException { + if (Link.getLinkType(url).equals(Link.FACEBOOK_LINK_TYPE)) { + return new URL("https://m." + url.substring(url.indexOf(Link.FACEBOOK_LINK_TYPE))); + } else if (Link.getLinkType(url).equals(Link.TWITTER_LINK_TYPE)) { + return new URL("https://" + url); + } + + return MainApp.class.getResource(FXML_FILE_FOLDER + "default.html"); + } + + //@@author /** * Returns a defensive copy of the current model. */ diff --git a/src/test/java/systemtests/ClearCommandSystemTest.java b/src/test/java/systemtests/ClearCommandSystemTest.java index 805a59784e29..9c6bdb00ebc5 100644 --- a/src/test/java/systemtests/ClearCommandSystemTest.java +++ b/src/test/java/systemtests/ClearCommandSystemTest.java @@ -77,6 +77,7 @@ private void assertCommandSuccess(String command, String expectedResultMessage, executeCommand(command); assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertStatusBarUnchangedExceptSyncStatus(); } @@ -96,6 +97,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } } diff --git a/src/test/java/systemtests/DeleteCommandSystemTest.java b/src/test/java/systemtests/DeleteCommandSystemTest.java index c0de78e4aba6..68de28314ecd 100644 --- a/src/test/java/systemtests/DeleteCommandSystemTest.java +++ b/src/test/java/systemtests/DeleteCommandSystemTest.java @@ -174,6 +174,7 @@ private void assertCommandSuccess(String command, Model expectedModel, String ex } assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertStatusBarUnchangedExceptSyncStatus(); } @@ -195,6 +196,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } } diff --git a/src/test/java/systemtests/EditCommandSystemTest.java b/src/test/java/systemtests/EditCommandSystemTest.java index 820933203dd9..351bdbd03eb6 100644 --- a/src/test/java/systemtests/EditCommandSystemTest.java +++ b/src/test/java/systemtests/EditCommandSystemTest.java @@ -251,6 +251,7 @@ private void assertCommandSuccess(String command, Model expectedModel, String ex expectedModel.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); if (expectedSelectedCardIndex != null) { assertSelectedCardChanged(expectedSelectedCardIndex); } else { @@ -277,6 +278,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } } diff --git a/src/test/java/systemtests/FindCommandSystemTest.java b/src/test/java/systemtests/FindCommandSystemTest.java index 0bde83c0444b..6ab8c9bee035 100644 --- a/src/test/java/systemtests/FindCommandSystemTest.java +++ b/src/test/java/systemtests/FindCommandSystemTest.java @@ -171,6 +171,7 @@ private void assertCommandSuccess(String command, Model expectedModel) { executeCommand(command); assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertStatusBarUnchanged(); } @@ -190,6 +191,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } } diff --git a/src/test/java/systemtests/FindWithTagCommandSystemTest.java b/src/test/java/systemtests/FindWithTagCommandSystemTest.java new file mode 100644 index 000000000000..3168fe07f782 --- /dev/null +++ b/src/test/java/systemtests/FindWithTagCommandSystemTest.java @@ -0,0 +1,193 @@ +package systemtests; + +import static org.junit.Assert.assertFalse; +import static seedu.address.commons.core.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.KEYWORD_MATCHING_ARMYBUDDY; + +import org.junit.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.FindWithTagCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.model.Model; + +//@@author KevinChuangCH +public class FindWithTagCommandSystemTest extends AddressBookSystemTest { + + @Test + public void find() { + /* Case: find multiple persons in address book, command with leading spaces and trailing spaces + * -> 2 persons found + */ + String command = " " + FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY + " "; + Model expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, BENSON, DANIEL); // Benson and Daniel have the tag "armyBuddy" + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: repeat previous find command where person list is displaying the persons we are finding + * -> 2 persons found + */ + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person where person list is not displaying the person we are finding -> 1 person found */ + command = FindWithTagCommand.COMMAND_WORD + " classmate"; + ModelHelper.setFilteredList(expectedModel, CARL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " owesMoney roommate"; + ModelHelper.setFilteredList(expectedModel, BENSON, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords in reversed order -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 keywords with 1 repeat -> 2 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney roommate"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find multiple persons in address book, 2 matching keywords and 1 non-matching keyword + * -> 2 persons found + */ + command = FindWithTagCommand.COMMAND_WORD + " roommate owesMoney NonMatchingKeyWord"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: undo previous find command -> rejected */ + command = UndoCommand.COMMAND_WORD; + String expectedResultMessage = UndoCommand.MESSAGE_FAILURE; + assertCommandFailure(command, expectedResultMessage); + + /* Case: redo previous find command -> rejected */ + command = RedoCommand.COMMAND_WORD; + expectedResultMessage = RedoCommand.MESSAGE_FAILURE; + assertCommandFailure(command, expectedResultMessage); + + /* Case: find same persons in address book after deleting 1 of them -> 1 person found */ + executeCommand(DeleteCommand.COMMAND_WORD + " 1"); + assertFalse(getModel().getAddressBook().getPersonList().contains(BENSON)); + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, keyword is same as tag but of different case -> 1 person found */ + command = FindWithTagCommand.COMMAND_WORD + " ArMyBuDdY"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, keyword is substring of tag -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " armyBud"; + ModelHelper.setFilteredList(expectedModel); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person in address book, name is substring of keyword -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " armyBuddies"; + ModelHelper.setFilteredList(expectedModel); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find person not in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " girlfriend"; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find name of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getName().fullName; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find phone number of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getPhone().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find address of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getAddress().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find email of person in address book -> 0 persons found */ + command = FindWithTagCommand.COMMAND_WORD + " " + DANIEL.getEmail().value; + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: find while a person is selected -> selected card deselected */ + showAllPersons(); + selectPerson(Index.fromOneBased(1)); + assertFalse(getPersonListPanel().getHandleToSelectedCard().getName().equals(DANIEL.getName().fullName)); + command = FindWithTagCommand.COMMAND_WORD + " roommate"; + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardDeselected(); + + /* Case: find person in empty address book -> 0 persons found */ + deleteAllPersons(); + command = FindWithTagCommand.COMMAND_WORD + " " + KEYWORD_MATCHING_ARMYBUDDY; + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, DANIEL); + assertCommandSuccess(command, expectedModel); + assertSelectedCardUnchanged(); + + /* Case: mixed case command word -> rejected */ + command = "FiNdTaG armyBuddy"; + assertCommandFailure(command, MESSAGE_UNKNOWN_COMMAND); + } + + /** + * Executes {@code command} and verifies that the command box displays an empty string, the result display + * box displays {@code Messages#MESSAGE_PERSONS_LISTED_OVERVIEW} with the number of people in the filtered list, + * and the model related components equal to {@code expectedModel}. + * These verifications are done by + * {@code AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model)}.
+ * Also verifies that the status bar remains unchanged, and the command box has the default style class, and the + * selected card updated accordingly, depending on {@code cardStatus}. + * @see AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model) + */ + private void assertCommandSuccess(String command, Model expectedModel) { + String expectedResultMessage = String.format( + MESSAGE_PERSONS_LISTED_OVERVIEW, expectedModel.getFilteredPersonList().size()); + + executeCommand(command); + assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); + assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); + assertStatusBarUnchanged(); + } + + /** + * Executes {@code command} and verifies that the command box displays {@code command}, the result display + * box displays {@code expectedResultMessage} and the model related components equal to the current model. + * These verifications are done by + * {@code AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model)}.
+ * Also verifies that the browser url, selected card and status bar remain unchanged, and the command box has the + * error style. + * @see AddressBookSystemTest#assertApplicationDisplaysExpected(String, String, Model) + */ + private void assertCommandFailure(String command, String expectedResultMessage) { + Model expectedModel = getModel(); + + executeCommand(command); + assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); + assertSelectedCardUnchanged(); + assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); + assertStatusBarUnchanged(); + } +} diff --git a/src/test/java/systemtests/HelpCommandSystemTest.java b/src/test/java/systemtests/HelpCommandSystemTest.java index 1aa4a5f294f4..43f6a5dd2021 100644 --- a/src/test/java/systemtests/HelpCommandSystemTest.java +++ b/src/test/java/systemtests/HelpCommandSystemTest.java @@ -63,6 +63,7 @@ public void openHelpWindow() { executeCommand(SelectCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); assertEquals("", getCommandBox().getInput()); assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertNotEquals(HelpCommand.SHOWING_HELP_MESSAGE, getResultDisplay().getText()); assertNotEquals(BrowserPanel.DEFAULT_PAGE, getBrowserPanel().getLoadedUrl()); assertListMatching(getPersonListPanel(), getModel().getFilteredPersonList()); diff --git a/src/test/java/systemtests/SelectCommandSystemTest.java b/src/test/java/systemtests/SelectCommandSystemTest.java index c7deb73454b1..f2a425b75999 100644 --- a/src/test/java/systemtests/SelectCommandSystemTest.java +++ b/src/test/java/systemtests/SelectCommandSystemTest.java @@ -127,6 +127,7 @@ private void assertCommandSuccess(String command, Index expectedSelectedCardInde } assertCommandBoxShowsDefaultStyle(); + assertResultDisplayShowsDefaultStyle(); assertStatusBarUnchanged(); } @@ -148,6 +149,7 @@ private void assertCommandFailure(String command, String expectedResultMessage) assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); assertSelectedCardUnchanged(); assertCommandBoxShowsErrorStyle(); + assertResultDisplayShowsErrorStyle(); assertStatusBarUnchanged(); } }