diff --git a/src/main/java/seedu/address/logic/commands/AttachCommand.java b/src/main/java/seedu/address/logic/commands/AttachCommand.java index 099421862f2..232cd41593b 100644 --- a/src/main/java/seedu/address/logic/commands/AttachCommand.java +++ b/src/main/java/seedu/address/logic/commands/AttachCommand.java @@ -58,10 +58,6 @@ public AttachCommand(Index index, List attachments) { @Override public CommandResult execute(Model model) throws CommandException { - // 1. Read the contents of the existing directory - // 2. Copy in all the new attachments, failing first if one of them fails - // 3. Add the attachments to the model - requireNonNull(model); List lastShownList = model.getFilteredPersonList(); diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..f3c6c6b3e9e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,251 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.FIELD_EMAIL; +import static seedu.address.logic.parser.CliSyntax.FIELD_GPA; +import static seedu.address.logic.parser.CliSyntax.FIELD_NAME; +import static seedu.address.logic.parser.CliSyntax.FIELD_PHONE; +import static seedu.address.logic.parser.CliSyntax.FIELD_PREVIOUS_GRADE; +import static seedu.address.logic.parser.CliSyntax.FIELD_STUDENT_NUMBER; +import static seedu.address.logic.parser.CliSyntax.FIELD_TAGS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILE; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.attachment.Attachment; +import seedu.address.model.person.Email; +import seedu.address.model.person.Gpa; +import seedu.address.model.person.IsBookmarked; +import seedu.address.model.person.IsHidden; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.PreviousGrade; +import seedu.address.model.person.StudentNumber; +import seedu.address.model.tag.Tag; + +/** + * Imports new applicants from the given file. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports applicants from the file " + + "specified, reading it in CSV format. The CSV header row is required, any\n " + + "additional fields other than those supported will be ignored, and any missing " + + "fields will be reported as an error.\n" + + "Supported fields: studentNo, name, phone, email, gpa, previousGrade, tags.\n" + + "Parameters: " + PREFIX_FILE + "FILE\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_FILE + "samples/applicants.csv"; + public static final String MESSAGE_IMPORT_SUCCESS = "Imported %1$s applicants successfully!"; + public static final String MESSAGE_IMPORT_NONE = "No applicants were imported."; + public static final String MESSAGE_IMPORT_FAILURE = "Failed to import %1$s applicants, on line %2$s."; + public static final String MESSAGE_FAILED_TO_OPEN = "Failed to open and load applicant file."; + public static final String MESSAGE_INVALID_FILE_FORMAT = "File format is invalid."; + public static final String MESSAGE_MISSING_FIELDS = "Missing fields from file: %1$s."; + + private static final String CELL_DELIM = ","; + private static final String DATA_ARRAY_DELIM = ";"; + private static final Set REQUIRED_FIELDS = Set.of( + FIELD_STUDENT_NUMBER, FIELD_NAME, + FIELD_PHONE, FIELD_EMAIL, FIELD_GPA, + FIELD_PREVIOUS_GRADE, FIELD_TAGS); + + private final Attachment attachment; + + /** + * Creates a new import command, which will import applicants listed in the + * given + * file (read in using CSV format). + * + * @param attachment The path of the file to be imported + */ + public ImportCommand(Attachment attachment) { + this.attachment = attachment; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List applicants = new ArrayList<>(); + List failureLineNos = new ArrayList<>(); + + Scanner scanner = null; + try { + scanner = new Scanner(attachment.file); + + if (!scanner.hasNextLine()) { + throw new CommandException(MESSAGE_INVALID_FILE_FORMAT); + } + String header = scanner.nextLine(); + String[] fields = header.split(CELL_DELIM); + + Set fieldSet = Set.of(fields); + if (!fieldSet.containsAll(REQUIRED_FIELDS)) { + Set requiredFieldsCopy = new HashSet<>(REQUIRED_FIELDS); + requiredFieldsCopy.removeAll(fieldSet); + throw new CommandException( + String.format(MESSAGE_MISSING_FIELDS, String.join(", ", requiredFieldsCopy))); + } + + Map fieldIndices = new HashMap<>(fields.length); + for (int i = 0; i < fields.length; i++) { + fieldIndices.put(fields[i], i); + } + + int lineNo = 1; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + String[] data = line.split(CELL_DELIM, -1); + + lineNo++; + + String studentNoString = data[fieldIndices.get(FIELD_STUDENT_NUMBER)]; + if (!StudentNumber.isValidStudentNumber(studentNoString)) { + failureLineNos.add(lineNo); + continue; + } + StudentNumber studentNo = new StudentNumber(studentNoString); + + String nameString = data[fieldIndices.get(FIELD_NAME)]; + if (!Name.isValidName(nameString)) { + failureLineNos.add(lineNo); + continue; + } + Name name = new Name(nameString); + + String phoneString = data[fieldIndices.get(FIELD_PHONE)]; + if (!Phone.isValidPhone(phoneString)) { + failureLineNos.add(lineNo); + continue; + } + Phone phone = new Phone(phoneString); + + String emailString = data[fieldIndices.get(FIELD_EMAIL)]; + if (!Email.isValidEmail(emailString)) { + failureLineNos.add(lineNo); + continue; + } + Email email = new Email(emailString); + + String gpaString = data[fieldIndices.get(FIELD_GPA)]; + double gpaDouble; + try { + gpaDouble = Double.parseDouble(gpaString); + } catch (NumberFormatException e) { + failureLineNos.add(lineNo); + continue; + } + if (!Gpa.isValidGpa(gpaDouble)) { + failureLineNos.add(lineNo); + continue; + } + Gpa gpa = new Gpa(gpaDouble); + + String previousGradeString = data[fieldIndices.get(FIELD_PREVIOUS_GRADE)]; + if (!PreviousGrade.isValidGrade(previousGradeString)) { + failureLineNos.add(lineNo); + continue; + } + PreviousGrade previousGrade = new PreviousGrade(previousGradeString); + + + String tagsString = data[fieldIndices.get(FIELD_TAGS)]; + String[] tagStrings = tagsString.split(DATA_ARRAY_DELIM); + Set tags = Set.of(); + if (tagStrings.length > 1 || tagStrings.length == 0 || !tagStrings[0].trim().equals("")) { + boolean isValid = List.of(tagStrings).stream().map(Tag::isValidTagName).allMatch(v -> v); + if (!isValid) { + failureLineNos.add(lineNo); + continue; + } + tags = List.of(tagStrings).stream().map(Tag::new).collect(Collectors.toSet()); + } + + Person applicant = new Person( + studentNo, name, phone, email, gpa, previousGrade, + Optional.empty(), Optional.empty(), tags, List.of(), + new IsHidden(false), new IsBookmarked(false)); + applicants.add(applicant); + } + } catch (FileNotFoundException e) { + throw new CommandException(MESSAGE_FAILED_TO_OPEN, e); + } finally { + if (scanner != null) { + scanner.close(); + } + } + + int newApplicantCount = applicants.size(); + for (Person applicant : applicants) { + if (model.hasPerson(applicant)) { + newApplicantCount--; + continue; + } + model.addPerson(applicant); + } + + String successMessage = null; + if (!applicants.isEmpty()) { + successMessage = String.format(MESSAGE_IMPORT_SUCCESS, newApplicantCount); + } + + String failureMessage = null; + if (!failureLineNos.isEmpty()) { + List lineNos = failureLineNos.stream().map(Object::toString).collect(Collectors.toList()); + failureMessage = String.format( + MESSAGE_IMPORT_FAILURE, + failureLineNos.size(), + String.join(", ", lineNos)); + } + + String message; + if (successMessage != null && failureMessage != null) { + message = successMessage + " " + failureMessage; + } else if (successMessage != null) { + message = successMessage; + } else if (failureMessage != null) { + message = failureMessage; + } else { + message = MESSAGE_IMPORT_NONE; + } + + return new CommandResult(message); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ImportCommand)) { + return false; + } + + ImportCommand otherImportCommand = (ImportCommand) other; + return attachment.equals(otherImportCommand.attachment); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("attachment", attachment) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b08daa95d3d..c57dd393c48 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -21,6 +21,7 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HideCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.ListBookmarkedCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.commands.ListHiddenCommand; @@ -84,6 +85,9 @@ public Command parseCommand(String userInput) throws ParseException { case AttachCommand.COMMAND_WORD: return new AttachCommandParser().parse(arguments); + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); diff --git a/src/main/java/seedu/address/logic/parser/AttachCommandParser.java b/src/main/java/seedu/address/logic/parser/AttachCommandParser.java index 077253bc3fe..3428416b982 100644 --- a/src/main/java/seedu/address/logic/parser/AttachCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AttachCommandParser.java @@ -14,19 +14,20 @@ import seedu.address.model.attachment.Attachment; /** - * Parses input arguments and creates a new AttachCommand object + * Parses input arguments and creates a new {@code AttachCommand} object */ public class AttachCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the + * {@code AttachCommand} and returns a {@code AttachCommand} object for + * execution. + * * @throws ParseException if the user input does not conform the expected format */ public AttachCommand parse(String args) throws ParseException { requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_FILE); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FILE); Index index; try { @@ -44,8 +45,10 @@ public AttachCommand parse(String args) throws ParseException { } /** - * Parses {@code Collection pathStrings} into a {@code List} if {@code pathStrings} is non-empty. - * If {@code pathStrings} contain only one element which is an empty string, it will be parsed into a + * Parses {@code Collection pathStrings} into a {@code List} if + * {@code pathStrings} is non-empty. + * If {@code pathStrings} contain only one element which is an empty string, it + * will be parsed into a * {@code List} containing zero tags. */ private List parseAttachments(Collection pathStrings) throws ParseException { diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..970bd6e7d4e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,24 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.attachment.Attachment; + +/** + * Parses input arguments and creates a new {@code ImportCommand} object + */ +public class ImportCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code ImportCommand} + * and returns a {@code ImportCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + requireNonNull(args); + Attachment attachment = ParserUtil.parseAttachment(args); + return new ImportCommand(attachment); + } +}