Skip to content

Commit

Permalink
[v1.2] Backup Feature (#47)
Browse files Browse the repository at this point in the history
* Model: Add backup property to UserPrefs model

Add backup file path to user preferences model.

Modified equals to compare backup location

Modified string builder to include backup path

* [Tests] Update JsonUserPrefsStorageTest

Modify ExtraValues json test file
Add `addressBookBackupFilePath` to typical user prefs json test file

* [Model] Add UserPrefs to Model

[Model]
UserPrefs is required to be in model manager as commands run can affect user preferences which should be part of model.

Refer to original AB4 architecture diagram

Added getUserPref method to Model interface

Fix UserPref model returning wrong path in getAddressBookBackupFilePath

[Tests]
Fix Stubbed ModelManager in AddCommandTest

* Add backup methods to AddressBookStorage

Make AddressBookStorage and XmlAddressBookStorage have the required backup methods

* Remove UserPrefs comparison from ModelManager

As long as AddressBook data are equal, userprefs can be different which is okay

* Additional tests for XmlAddressBookStorage

* Add basic backup command

Backup Command currently calls model.backupAddressBook, perhaps we should raise a backup request event from command direct and get handled by storage manager directly as we do not need to modify the model

* Add sample tests for backup command

* CheckStyle Fixes

* More CheckStyle Fixes

* More checkstyle fixes :(

* Make backup generic to allow future extensions

More tests
Bug Fixes

* Resolve conflicts

* Checkstyle fixes

* Add tests to fix coverage

* Fix BackupCommandParser tests

* Fixs bugs

* Last try to fix test on linux

* Remove test

Impossible to make exception to be thrown on linux even with mocking for invalid paths

* Remove unused imports

* Implement online backup

Create generic online storage module
Implement working backup to github gist
Modify backup command to accept github backup

* Checkstyle and Bug Fixes

* Temp Bug Fix
  • Loading branch information
QzSG authored and ChenSongJian committed Oct 10, 2018
1 parent 25088fc commit 8c48fea
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
implementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.3.0'
implementation group: 'com.sun.xml.bind', name: 'jaxb-core', version: '2.3.0'
implementation group: 'javax.activation', name: 'activation', version: '1.1.1'
implementation group: 'org.kohsuke', name: 'github-api', version: '1.94'

testImplementation group: 'junit', name: 'junit', version: '4.12'
testImplementation group: 'org.testfx', name: 'testfx-core', version: testFxVersion, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package seedu.address.commons.events.storage;

import java.util.Optional;

import seedu.address.commons.events.BaseEvent;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.storage.OnlineStorage;

//@@author QzSG
/** Indicates a request for online backup*/
public class OnlineBackupEvent extends BaseEvent {

public final OnlineStorage.OnlineStorageType target;
public final ReadOnlyAddressBook data;
public final String fileName;
public final Optional<String> authToken;

public OnlineBackupEvent(OnlineStorage.OnlineStorageType target, ReadOnlyAddressBook data,
String fileName, Optional<String> authToken) {
this.target = target;
this.data = data;
this.fileName = fileName;
this.authToken = authToken;
}

@Override
public String toString() {
return "Saving data online";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package seedu.address.commons.exceptions;

/**
* Signals that some online backup failed for some reason.
*/
public class OnlineBackupFailureException extends Exception {
/**
* @param message should contain relevant information on failure reason(s)
*/
public OnlineBackupFailureException(String message) {
super(message);
}

/**
* @param message should contain relevant information on failure reason(s)
* @param cause of the main exception
*/
public OnlineBackupFailureException(String message, Throwable cause) {
super(message, cause);
}
}
19 changes: 19 additions & 0 deletions src/main/java/seedu/address/commons/util/XmlUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static java.util.Objects.requireNonNull;

import java.io.FileNotFoundException;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;

Expand Down Expand Up @@ -68,4 +69,22 @@ public static <T> void saveDataToFile(Path file, T data) throws FileNotFoundExce
m.marshal(data, file.toFile());
}

/**
* Converts the data in the file to a sting.
*
* @throws JAXBException Thrown if there is an error during converting the data
* into xml and returning the string.
*/
public static <T> String convertContentToString(T data) throws JAXBException {
requireNonNull(data);

StringWriter stringWriter = new StringWriter();
JAXBContext context = JAXBContext.newInstance(data.getClass());
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

m.marshal(data, stringWriter);
return stringWriter.toString();
}

}
49 changes: 42 additions & 7 deletions src/main/java/seedu/address/logic/commands/BackupCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
import java.nio.file.Path;
import java.util.Optional;

import javafx.concurrent.Task;
import seedu.address.commons.core.EventsCenter;
import seedu.address.commons.events.storage.OnlineBackupEvent;
import seedu.address.logic.CommandHistory;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.storage.OnlineStorage;

//@@author QzSG
/**
Expand All @@ -18,26 +23,40 @@ public class BackupCommand extends Command {

public static final String MESSAGE_USAGE = COMMAND_WORD
+ ": Backups student planner data to location specified (backups to default data path if not provided)\n"
+ "Parameters: [PATH] (must be a writable path)\n"
+ "Example: " + COMMAND_WORD + " data\\addressbook.bak";
+ "Parameters: [github authToken] OR [PATH] (must be a writable path)\n"
+ "Example: " + COMMAND_WORD + " data\\addressbook.bak OR\n"
+ "Example: " + COMMAND_WORD + " github my_personal_access_token";

public static final String MESSAGE_SUCCESS = "Initiating Backup to %s";

private Optional<Path> backupPath;
private boolean isLocal = true;
private OnlineStorage.OnlineStorageType target;
private Optional<String> authToken;

/**
* Creates an AddCommand to add the specified {@code Person}
* Creates a BackupCommand to backup data to storage
*/
public BackupCommand(Optional<Path> backupPath) {
public BackupCommand(Optional<Path> backupPath, boolean isLocal,
Optional<OnlineStorage.OnlineStorageType> target, Optional<String> authToken) {
this.backupPath = backupPath;
this.isLocal = isLocal;
this.target = target.orElse(OnlineStorage.OnlineStorageType.GITHUB);
this.authToken = authToken;

}

@Override
public CommandResult execute(Model model, CommandHistory history) {
requireNonNull(model);
model.backupAddressBook(retrievePath(model));
System.out.println(retrievePath(model));
return new CommandResult(String.format(MESSAGE_SUCCESS, retrievePath(model).toString()));
if (isLocal) {
model.backupAddressBook(retrievePath(model));
return new CommandResult(String.format(MESSAGE_SUCCESS, retrievePath(model).toString()));
} else {
new Thread(onlineBackupTask(model.getAddressBook())).start();
return new CommandResult(String.format(MESSAGE_SUCCESS, "GitHub Gists"));
}

}

private Path retrievePath(Model model) {
Expand All @@ -50,4 +69,20 @@ public boolean equals(Object other) {
|| (other instanceof BackupCommand // instanceof handles nulls
&& backupPath.equals(((BackupCommand) other).backupPath));
}

/**
* Background task to prevent main ui thread from freezing during online backup
* @param addressBook
* @return Task that can be started to run online backup on non ui thread
*/
private Task onlineBackupTask(ReadOnlyAddressBook addressBook) {
Task task = new Task<Void>() {
@Override public Void call() {
EventsCenter.getInstance().post(new OnlineBackupEvent(target, addressBook,
"AddressBook.bak", authToken));
return null;
}
};
return task;
}
}
30 changes: 26 additions & 4 deletions src/main/java/seedu/address/logic/parser/BackupCommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import seedu.address.logic.commands.BackupCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.storage.OnlineStorage;


//@@author QzSG
Expand All @@ -21,16 +24,35 @@ public class BackupCommandParser implements Parser<BackupCommand> {
*/
public BackupCommand parse(String args) throws ParseException {
try {
String backupPathString = args.trim();
if (backupPathString.isEmpty()) {
return new BackupCommand(Optional.empty());
String trimmedArgs = args.trim();
if (trimmedArgs.isEmpty()) {
return new BackupCommand(Optional.empty(), true, Optional.empty(), Optional.empty());
} else {
return new BackupCommand(ParserUtil.parsePath(backupPathString));
return parseArguments(trimmedArgs);
}
} catch (Exception pe) {
throw new ParseException(
String.format(MESSAGE_INVALID_COMMAND_FORMAT, BackupCommand.MESSAGE_USAGE), pe);
}
}

/**
* Parses extra arguments given by the user
* @param args
* @return BackupCommand for execution
* @throws ParseException
*/
private BackupCommand parseArguments(String args) throws ParseException {
List<String> argumentList = Arrays.asList(args.split(" ", 0));
if (argumentList.size() == 1) {
return new BackupCommand(ParserUtil.parsePath(argumentList.get(0)), true,
Optional.empty(), Optional.empty());
}
if (argumentList.size() == 2 && argumentList.get(0).toLowerCase().equals("github")) {
return new BackupCommand(Optional.empty(), false,
Optional.ofNullable(OnlineStorage.OnlineStorageType.GITHUB),
Optional.ofNullable(argumentList.get(1)));
}
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, BackupCommand.MESSAGE_USAGE));
}
}
49 changes: 49 additions & 0 deletions src/main/java/seedu/address/storage/GitHubStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//@@author QzSG
package seedu.address.storage;

import static java.util.Objects.requireNonNull;

import java.io.IOException;

import org.kohsuke.github.GHGist;
import org.kohsuke.github.GHGistBuilder;
import org.kohsuke.github.GitHub;

import seedu.address.commons.exceptions.OnlineBackupFailureException;

/**
* A class to handle saving data to Github Gists.
*/
public class GitHubStorage implements OnlineStorage {

private static GitHub gitHub = null;
private String authToken = null;

public GitHubStorage(String authToken) {
requireNonNull(authToken);
this.authToken = authToken;
}

@Override
public void saveContentToStorage(String content, String fileName) throws IOException, OnlineBackupFailureException {
throw new UnsupportedOperationException("This online storage does not "
+ "support saveContentToStorage with 2 variables");
}

@Override
public void saveContentToStorage(String content, String fileName, String description)
throws IOException, OnlineBackupFailureException {
requireNonNull(content);
requireNonNull(fileName);

gitHub = GitHub.connectUsingOAuth(authToken);
GHGistBuilder ghGistBuilder = buildGistFromContent(content, fileName, description);
GHGist ghGist = ghGistBuilder.create();
}

private GHGistBuilder buildGistFromContent(String content, String fileName, String description) {
GHGistBuilder ghGistBuilder = new GHGistBuilder(gitHub);
ghGistBuilder.public_(false).description(description).file(fileName, content);
return ghGistBuilder;
}
}
38 changes: 38 additions & 0 deletions src/main/java/seedu/address/storage/OnlineStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package seedu.address.storage;

//@@author QzSG

import java.io.IOException;

import seedu.address.commons.exceptions.OnlineBackupFailureException;

/**
* API of the OnlineStorage component
*/
public interface OnlineStorage {

/**
* Enum types for support online storage
*/
enum OnlineStorageType {
GITHUB
}

/**
* Saves the given {@code content} to the online storage.
* @param content cannot be null.
* @param fileName cannot be null.
* @throws OnlineBackupFailureException if there was any problem saving to online storage.
*/
void saveContentToStorage(String content, String fileName) throws IOException, OnlineBackupFailureException;

/**
* Saves the given {@code content} to the online storage.
* @param content cannot be null.
* @param fileName cannot be null.
* @param description can be null.
* @throws OnlineBackupFailureException if there was any problem saving to online storage.
*/
void saveContentToStorage(String content, String fileName, String description)
throws IOException, OnlineBackupFailureException;
}
44 changes: 44 additions & 0 deletions src/main/java/seedu/address/storage/StorageManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Logger;
import javax.xml.bind.JAXBException;

import com.google.common.eventbus.Subscribe;

Expand All @@ -12,10 +13,14 @@
import seedu.address.commons.events.model.AddressBookChangedEvent;
import seedu.address.commons.events.model.AddressBookLocalBackupEvent;
import seedu.address.commons.events.storage.DataSavingExceptionEvent;
import seedu.address.commons.events.storage.OnlineBackupEvent;
import seedu.address.commons.exceptions.DataConversionException;
import seedu.address.commons.exceptions.OnlineBackupFailureException;
import seedu.address.commons.util.XmlUtil;
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.UserPrefs;


/**
* Manages storage of AddressBook data in local storage.
*/
Expand All @@ -25,6 +30,7 @@ public class StorageManager extends ComponentManager implements Storage {
private AddressBookStorage addressBookStorage;
private UserPrefsStorage userPrefsStorage;

private GitHubStorage gitHubStorage;

public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) {
super();
Expand Down Expand Up @@ -105,4 +111,42 @@ public void handleAddressBookLocalBackupEvent(AddressBookLocalBackupEvent event)
raise(new DataSavingExceptionEvent(e));
}
}

// ================ GitHub Storage methods ==============================
/*
Listens directly to BackupCommand
*/
@SuppressWarnings("unused")
@Subscribe
public void handleOnlineBackupEvent(OnlineBackupEvent event) {
logger.info(LogsCenter.getEventHandlingLogMessage(event, "Saving data to online storage"));
try {
backupOnline(event.target, event.data, event.fileName, event.authToken);
} catch (IOException | OnlineBackupFailureException | JAXBException e) {
raise(new DataSavingExceptionEvent(e));
}
}

/**
* Performs online backup to supported online storage
* @param target
* @param data
* @param fileName
* @param authToken
* @throws IOException
* @throws OnlineBackupFailureException
* @throws JAXBException
*/
private void backupOnline(OnlineStorage.OnlineStorageType target, ReadOnlyAddressBook data,
String fileName, Optional<String> authToken)
throws IOException, OnlineBackupFailureException, JAXBException {
switch(target) {
case GITHUB:
default:
gitHubStorage = new GitHubStorage(
authToken.orElseThrow(() -> new OnlineBackupFailureException("Invalid auth token received")));
gitHubStorage.saveContentToStorage(XmlUtil.convertContentToString(
new XmlSerializableAddressBook(data)), fileName, "Address Book Backup");
}
}
}
Loading

0 comments on commit 8c48fea

Please sign in to comment.