Skip to content

Commit

Permalink
Merge pull request #59 from Jollybomber/import-export-storage
Browse files Browse the repository at this point in the history
Add import function
  • Loading branch information
Plishh authored Oct 17, 2024
2 parents c34617c + d448c71 commit f8c7c89
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 9 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies {
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion

testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion
implementation group: 'com.opencsv', name: 'opencsv', version: '5.9'
}

shadowJar {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package seedu.address.commons.exceptions;

/**
* Signals that the format of the import file has errors.
*/
public class ImproperFormatException extends Exception {
/**
* @param message should contain relevant information on the error(s)
*/
public ImproperFormatException(String message) {
super(message);
}
}
79 changes: 79 additions & 0 deletions src/main/java/seedu/address/logic/commands/ImportCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package seedu.address.logic.commands;

import static java.util.Objects.requireNonNull;
import static seedu.address.logic.parser.CliSyntax.PREFIX_FILEPATH;

import java.util.ArrayList;

import seedu.address.commons.exceptions.ImproperFormatException;
import seedu.address.commons.util.ToStringBuilder;
import seedu.address.logic.CommandHistory;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
import seedu.address.storage.CsvImport;

/**
* Adds a list of persons to the address book provided by an import file.
*/
public class ImportCommand extends Command {
public static final String COMMAND_WORD = "import";

public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a list of persons provided in"
+ " the provided file."
+ "Example: " + COMMAND_WORD + " "
+ PREFIX_FILEPATH + "~/data/test.txt";

public static final String MESSAGE_SUCCESS = "%d persons added";
public static final String MESSAGE_FAILED = "Rows that could not be added: %s";

private final String importFilePath;
public ImportCommand(String importFilePath) {
this.importFilePath = importFilePath;
}

@Override
public CommandResult execute(Model model, CommandHistory history) throws CommandException {
requireNonNull(model);

CsvImport importer = new CsvImport(importFilePath);
int personsAdded;
try {
personsAdded = importer.readCsv(model);
} catch (ImproperFormatException e) {
throw new CommandException(e.getMessage());
}

ArrayList<Integer> personsFailed = importer.getFailed();
model.commitAddressBook();
if (!importer.hasFailures()) {
return new CommandResult(String.format(MESSAGE_SUCCESS, personsAdded));
} else if (personsAdded == 0) {
return new CommandResult(String.format(MESSAGE_FAILED, personsFailed));
} else {
return new CommandResult(String.format(MESSAGE_SUCCESS, personsAdded)
+ String.format(MESSAGE_FAILED, personsFailed));
}
}

@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 importFilePath.equals(otherImportCommand.importFilePath);
}

@Override
public String toString() {
return new ToStringBuilder(this)
.add("toImport", importFilePath)
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.commands.HelpCommand;
import seedu.address.logic.commands.HistoryCommand;
import seedu.address.logic.commands.ImportCommand;
import seedu.address.logic.commands.ListCommand;
import seedu.address.logic.commands.RedoCommand;
import seedu.address.logic.commands.UndoCommand;
Expand Down Expand Up @@ -97,6 +98,9 @@ public Command parseCommand(String userInput) throws ParseException {
case ViewCommand.COMMAND_WORD:
return new ViewCommandParser().parse(arguments);

case ImportCommand.COMMAND_WORD:
return new ImportCommandParser().parse(arguments);

case ViewTuteeChartCommand.COMMAND_WORD:
return new ViewTuteeChartCommand();

Expand Down
1 change: 1 addition & 0 deletions src/main/java/seedu/address/logic/parser/CliSyntax.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class CliSyntax {
public static final Prefix PREFIX_EMAIL = new Prefix("-email ");
public static final Prefix PREFIX_ADDRESS = new Prefix("-address ");
public static final Prefix PREFIX_TAG = new Prefix("t/"); // TODO change?
public static final Prefix PREFIX_FILEPATH = new Prefix("-filepath ");


}
44 changes: 44 additions & 0 deletions src/main/java/seedu/address/logic/parser/ImportCommandParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package seedu.address.logic.parser;

import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.parser.CliSyntax.PREFIX_FILEPATH;

import java.util.stream.Stream;

import seedu.address.logic.commands.ImportCommand;
import seedu.address.logic.parser.exceptions.ParseException;

/**
* Parses input arguments and creates a new ImportCommand object
*/
public class ImportCommandParser implements Parser<ImportCommand> {

/**
* Parses the given {@code String} of arguments in the context of the AddCommand
* and returns an AddCommand object for execution.
* @throws ParseException if the user input does not conform the expected format
*/
public ImportCommand parse(String args) throws ParseException {
ArgumentMultimap argMultimap =
ArgumentTokenizer.tokenize(args, PREFIX_FILEPATH);

if (!arePrefixesPresent(argMultimap, PREFIX_FILEPATH)
|| !argMultimap.getPreamble().isEmpty()) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE));
}

argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_FILEPATH);
String filepath = ParserUtil.parseFilepath(argMultimap.getValue(PREFIX_FILEPATH).get());

return new ImportCommand(filepath);
}

/**
* 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());
}

}
18 changes: 18 additions & 0 deletions src/main/java/seedu/address/logic/parser/ParserUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static java.util.Objects.requireNonNull;

import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
Expand Down Expand Up @@ -137,4 +138,21 @@ public static Set<Tag> parseTags(Collection<String> tags) throws ParseException
}
return tagSet;
}

/**
* Parses a {@code String filepath}.
* Leading and trailing whitespaces will be trimmed.
*
* @throws ParseException if the given {@code filepath} is invalid.
*/
public static String parseFilepath(String filepath) throws ParseException {
requireNonNull(filepath);
String trimmedFilepath = filepath.trim();
String transformedFilePath = trimmedFilepath.replaceFirst("^~", System.getProperty("user.home"));

if (!new File(transformedFilePath).isFile()) {
throw new ParseException("File does not exist!");
}
return transformedFilePath;
}
}
148 changes: 148 additions & 0 deletions src/main/java/seedu/address/storage/CsvImport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package seedu.address.storage;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.opencsv.CSVReader;
import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.exceptions.CsvValidationException;

import seedu.address.commons.exceptions.IllegalValueException;
import seedu.address.commons.exceptions.ImproperFormatException;
import seedu.address.model.Model;

/**
* Handles the import of CSV data into the application.
* This class reads a specified CSV file and converts its contents into
* JsonAdaptedPerson objects, which can then be added to a Model.
*/
public class CsvImport {
private static final String INCORRECT_HEADERS = "Your file is missing or has extra headers. "
+ "Please use the following headers: name,phone,email,address,hours,tags,role ";
private static final String INCORRECT_ROWS = "Some rows have an incorrect number of entries. "
+ "The expected number of entries is %d. The rows that failed are: %s";
private static final int HEADER_COUNT = 7;
private final String importFilePath;
private final ArrayList<Integer> failed;

/**
* Constructs a CsvImport instance with the specified file path.
*
* @param importFilePath The path of the CSV file to be imported.
*/
public CsvImport(String importFilePath) {
this.importFilePath = importFilePath;
this.failed = new ArrayList<>();
}

/**
* Reads the CSV file and imports the data into the provided model.
*
* @param model The model to which the imported JsonAdaptedPerson objects will be added.
* @return The number of successful imports.
*/
public int readCsv(Model model) throws ImproperFormatException {
FileReader reader = null;
FileReader headerReader = null;
FileReader rowReader = null;
try {
reader = new FileReader(importFilePath);
headerReader = new FileReader(importFilePath);
rowReader = new FileReader(importFilePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
}

assert reader != null;
if (!validateHeaders(headerReader)) {
throw new ImproperFormatException(INCORRECT_HEADERS);
} else if (!validateCsv(rowReader)) {
throw new ImproperFormatException(String.format(INCORRECT_ROWS, HEADER_COUNT, failed));
}
List<JsonAdaptedPerson> personList = new CsvToBeanBuilder<JsonAdaptedPerson>(reader)
.withType(JsonAdaptedPerson.class).build().parse();

int success = 0;
for (JsonAdaptedPerson p : personList) {
try {
if (model.hasPerson(p.toModelType())) {
failed.add(personList.indexOf(p));
} else {
model.addPerson(p.toModelType());
success++;
}
} catch (IllegalValueException e) {
failed.add(personList.indexOf(p));
}
}
return success;
}

public ArrayList<Integer> getFailed() {
return failed;
}

public boolean hasFailures() {
return !failed.isEmpty();
}

private boolean validateHeaders(FileReader reader) {
CSVReader csvReader = new CSVReader(reader);

ArrayList<String> expectedHeaders = new ArrayList<>();
expectedHeaders.add("name");
expectedHeaders.add("phone");
expectedHeaders.add("email");
expectedHeaders.add("address");
expectedHeaders.add("hours");
expectedHeaders.add("tags");
expectedHeaders.add("role");

try {
ArrayList<String> actualHeaders = new ArrayList<>(List.of(csvReader.peek()));
if (actualHeaders.isEmpty()) {
return false;
}

// Check for missing headers
for (String expectedHeader : expectedHeaders) {
if (!actualHeaders.contains(expectedHeader)) {
return false;
}
}

// Check for extra headers
for (String actualHeader : actualHeaders) {
if (!expectedHeaders.contains(actualHeader)) {
return false;
}
}
} catch (IOException e) {
e.printStackTrace();

}
return true;
}

private boolean validateCsv(FileReader reader) {
CSVReader csvReader = new CSVReader(reader);
try {
csvReader.skip(1);
int lineCount = 1;
String[] row = csvReader.readNext();
while (row != null) {
if (row.length != HEADER_COUNT) {
failed.add(lineCount);
}
lineCount++;
row = csvReader.readNext();
}
} catch (IOException | CsvValidationException e) {
e.printStackTrace();
}
return failed.isEmpty();
}
}
Loading

0 comments on commit f8c7c89

Please sign in to comment.