tagList) {
- return tagList.stream().map(this::convertToCloudTag).collect(Collectors.toCollection(ArrayList::new));
+ return tagList.stream()
+ .map(this::convertToCloudTag)
+ .collect(Collectors.toCollection(ArrayList::new));
}
private Person convertToPerson(CloudPerson cloudPerson) {
diff --git a/src/main/java/address/sync/SyncManager.java b/src/main/java/address/sync/SyncManager.java
index 002fc664..5cdd4375 100644
--- a/src/main/java/address/sync/SyncManager.java
+++ b/src/main/java/address/sync/SyncManager.java
@@ -43,9 +43,9 @@ public class SyncManager extends ComponentManager {
* @param config should have updateInterval (milliseconds) and simulateUnreliableNetwork set
* @param activeAddressBookName name of active addressbook to start with
*/
- public SyncManager(Config config, String activeAddressBookName) {
- this(config, new RemoteManager(config), Executors.newCachedThreadPool(),
- Executors.newScheduledThreadPool(1), activeAddressBookName);
+ public SyncManager(RemoteManager remoteManager, Config config, String activeAddressBookName) {
+ this(config, remoteManager, Executors.newCachedThreadPool(),
+ Executors.newSingleThreadScheduledExecutor(), activeAddressBookName);
}
/**
@@ -96,10 +96,9 @@ public void setActiveAddressBook(String activeAddressBookName) {
*/
public void start() {
logger.info("Starting sync manager.");
- long initialDelay = 300; // temp fix for issue #66
Runnable syncTask = new GetUpdatesFromRemoteTask(remoteManager, this::raise, this::getActiveAddressBook);
- logger.debug("Scheduling synchronization task with interval of {} milliseconds", config.updateInterval);
- scheduler.scheduleWithFixedDelay(syncTask, initialDelay, config.updateInterval, TimeUnit.MILLISECONDS);
+ logger.debug("Scheduling synchronization task with interval of {} milliseconds", config.getUpdateInterval());
+ scheduler.scheduleWithFixedDelay(syncTask, 0, config.getUpdateInterval(), TimeUnit.MILLISECONDS);
}
public void stop() {
diff --git a/src/main/java/address/sync/cloud/CloudFileHandler.java b/src/main/java/address/sync/cloud/CloudFileHandler.java
index 7c2f3cff..9345e7c3 100644
--- a/src/main/java/address/sync/cloud/CloudFileHandler.java
+++ b/src/main/java/address/sync/cloud/CloudFileHandler.java
@@ -14,24 +14,19 @@ public class CloudFileHandler {
private static final AppLogger logger = LoggerManager.getLogger(CloudFileHandler.class);
private static final String CLOUD_DIRECTORY = "cloud/";
- public CloudAddressBook readCloudAddressBookFromFile(String addressBookName) throws FileNotFoundException,
+ public CloudAddressBook readCloudAddressBookFromExternalFile(String cloudDataFilePath) throws FileNotFoundException,
+ DataConversionException {
+ File cloudFile = new File(cloudDataFilePath);
+ return readFromCloudFile(cloudFile);
+ }
+
+ public CloudAddressBook readCloudAddressBook(String addressBookName) throws FileNotFoundException,
DataConversionException {
File cloudFile = getCloudDataFile(addressBookName);
- try {
- logger.debug("Reading from cloud file '{}'.", cloudFile.getName());
- CloudAddressBook CloudAddressBook = XmlUtil.getDataFromFile(cloudFile, CloudAddressBook.class);
- if (CloudAddressBook.getName() == null) throw new DataConversionException("AddressBook name is null.");
- return CloudAddressBook;
- } catch (FileNotFoundException e) {
- logger.warn("Cloud file '{}' not found.", cloudFile.getName());
- throw e;
- } catch (DataConversionException e) {
- logger.warn("Error reading from cloud file '{}'.", cloudFile.getName());
- throw e;
- }
+ return readFromCloudFile(cloudFile);
}
- public void writeCloudAddressBookToFile(CloudAddressBook CloudAddressBook) throws FileNotFoundException,
+ public void writeCloudAddressBook(CloudAddressBook CloudAddressBook) throws FileNotFoundException,
DataConversionException {
String addressBookName = CloudAddressBook.getName();
File cloudFile = getCloudDataFile(addressBookName);
@@ -44,26 +39,103 @@ public void writeCloudAddressBookToFile(CloudAddressBook CloudAddressBook) throw
}
}
- public void createCloudAddressBookFile(String addressBookName) throws IOException, DataConversionException,
+ /**
+ * Attempts to create a file with an empty address book
+ * Deletes any existing file on the same path
+ *
+ * @param addressBookName
+ * @throws IOException
+ * @throws DataConversionException
+ */
+ public void initializeAddressBook(String addressBookName) throws IOException, DataConversionException {
+ File cloudFile = getCloudDataFile(addressBookName);
+ if (cloudFile.exists()) {
+ cloudFile.delete();
+ }
+
+ createCloudFile(new CloudAddressBook(addressBookName));
+ }
+
+ /**
+ * Attempts to create an empty address book on the cloud
+ * Fails if address book already exists
+ *
+ * @param addressBookName
+ * @throws IOException
+ * @throws DataConversionException
+ * @throws IllegalArgumentException if cloud file already exists
+ */
+ public void createAddressBook(String addressBookName) throws IOException, DataConversionException,
+ IllegalArgumentException {
+ createCloudFile(new CloudAddressBook(addressBookName));
+ }
+
+ /**
+ * Attempts to create an empty address book on the cloud if it does not exist
+ *
+ * @param addressBookName
+ * @throws IOException
+ * @throws DataConversionException
+ * @throws IllegalArgumentException
+ */
+ public void createAddressBookIfAbsent(String addressBookName) throws IOException, DataConversionException,
IllegalArgumentException {
File cloudFile = getCloudDataFile(addressBookName);
+ if (cloudFile.exists()) return;
+ try {
+ createCloudFile(new CloudAddressBook(addressBookName));
+ } catch (IllegalArgumentException e) {
+ assert false : "Error in logic: createCloudFile should not be called since address book is present";
+ }
+ }
+
+ private CloudAddressBook readFromCloudFile(File cloudFile) throws FileNotFoundException, DataConversionException {
+ try {
+ logger.debug("Reading from cloud file '{}'.", cloudFile.getName());
+ CloudAddressBook cloudAddressBook = XmlUtil.getDataFromFile(cloudFile, CloudAddressBook.class);
+ if (cloudAddressBook.getName() == null) throw new DataConversionException("AddressBook name is null.");
+ return cloudAddressBook;
+ } catch (FileNotFoundException e) {
+ logger.warn("Cloud file '{}' not found.", cloudFile.getName());
+ throw e;
+ } catch (DataConversionException e) {
+ logger.warn("Error reading from cloud file '{}'.", cloudFile.getName());
+ throw e;
+ }
+ }
+
+ /**
+ * Attempts to create the cloud file in the cloud directory, containing an empty address book
+ * File will be named the same as the address book
+ *
+ * The cloud directory will also be created if it does not exist
+ *
+ * @param cloudAddressBook
+ * @throws IOException
+ * @throws DataConversionException
+ * @throws IllegalArgumentException if cloud file already exists
+ */
+ private void createCloudFile(CloudAddressBook cloudAddressBook) throws IOException, DataConversionException, IllegalArgumentException {
+ File cloudFile = getCloudDataFile(cloudAddressBook.getName());
if (cloudFile.exists()) {
- logger.warn("Cannot create an addressbook that already exists: '{}'.", addressBookName);
- throw new IllegalArgumentException("AddressBook '" + addressBookName + "' already exists!");
+ logger.warn("Cannot create an address book that already exists: '{}'.", cloudAddressBook.getName());
+ throw new IllegalArgumentException("AddressBook '" + cloudAddressBook.getName() + "' already exists!");
}
+
File cloudDirectory = new File(CLOUD_DIRECTORY);
if (!cloudDirectory.exists() && !cloudDirectory.mkdir()) {
logger.warn("Error creating directory: '{}'", CLOUD_DIRECTORY);
throw new IOException("Error creating directory: " + CLOUD_DIRECTORY);
}
+
if (!cloudFile.createNewFile()) {
- logger.warn("Error creating cloud file: '{}'", getCloudDataFilePath(addressBookName));
- throw new IOException("Error creating cloud file for addressbook: " + getCloudDataFilePath(addressBookName));
+ logger.warn("Error creating cloud file: '{}'", getCloudDataFilePath(cloudAddressBook.getName()));
+ throw new IOException("Error creating cloud file for address book: " + getCloudDataFilePath(cloudAddressBook.getName()));
}
- writeCloudAddressBookToFile(new CloudAddressBook(addressBookName));
+ writeCloudAddressBook(cloudAddressBook);
}
private File getCloudDataFile(String addressBookName) {
diff --git a/src/main/java/address/sync/cloud/CloudSimulator.java b/src/main/java/address/sync/cloud/CloudSimulator.java
index b2499c29..1089e738 100644
--- a/src/main/java/address/sync/cloud/CloudSimulator.java
+++ b/src/main/java/address/sync/cloud/CloudSimulator.java
@@ -13,7 +13,6 @@
import java.net.HttpURLConnection;
import java.time.LocalDateTime;
import java.util.*;
-import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@@ -23,82 +22,62 @@
* Requests for a full list of objects should be done in pages. Responses
* will include first page/prev page/next page/last page if they exist.
*
- * Any bad requests due to inappropriate parameters will still consume API
- * usage.
- *
- * In addition, data returned by this cloud may be modified due to
- * simulated corruption or its responses may have significant delays,
- * if the cloud is initialized with an unreliable network parameter
+ * Providing previous request's eTag may return a NOT_MODIFIED response if the response's eTag has not changed.
+ * All requests (including bad ones) will consume API, unless it is a response with NOT_MODIFIED.
*/
-public class CloudSimulator implements ICloudSimulator {
+public class CloudSimulator implements IRemote {
private static final AppLogger logger = LoggerManager.getLogger(CloudSimulator.class);
private static final int API_QUOTA_PER_HOUR = 5000;
- private static final Random RANDOM_GENERATOR = new Random();
- private static final double FAILURE_PROBABILITY = 0.1;
- private static final double NETWORK_DELAY_PROBABILITY = 0.2;
- private static final int MIN_DELAY_IN_SEC = 1;
- private static final int DELAY_RANGE = 5;
- private static final double MODIFY_PERSON_PROBABILITY = 0.1;
- private static final double MODIFY_TAG_PROBABILITY = 0.05;
- private static final double ADD_PERSON_PROBABILITY = 0.05;
- private static final double ADD_TAG_PROBABILITY = 0.025;
- private static final int MAX_NUM_PERSONS_TO_ADD = 2;
- private CloudRateLimitStatus cloudRateLimitStatus;
- private boolean shouldSimulateUnreliableNetwork;
- private CloudFileHandler fileHandler;
-
- public CloudSimulator(CloudFileHandler fileHandler, CloudRateLimitStatus cloudRateLimitStatus,
- boolean shouldSimulateUnreliableNetwork) {
+
+ protected CloudRateLimitStatus cloudRateLimitStatus;
+ protected CloudFileHandler fileHandler;
+
+ protected CloudSimulator(CloudFileHandler fileHandler, CloudRateLimitStatus cloudRateLimitStatus) {
this.fileHandler = fileHandler;
this.cloudRateLimitStatus = cloudRateLimitStatus;
- this.shouldSimulateUnreliableNetwork = shouldSimulateUnreliableNetwork;
}
public CloudSimulator(Config config) {
fileHandler = new CloudFileHandler();
cloudRateLimitStatus = new CloudRateLimitStatus(API_QUOTA_PER_HOUR);
- this.shouldSimulateUnreliableNetwork = config.simulateUnreliableNetwork;
cloudRateLimitStatus.restartQuotaTimer();
+ try {
+ fileHandler.createAddressBookIfAbsent(config.getAddressBookName());
+ } catch (IOException | DataConversionException e) {
+ logger.fatal("Error initializing cloud file for '{}'", config.getAddressBookName());
+ assert false : "Error initializing cloud file: " + config.getAddressBookName();
+ }
}
/**
* Attempts to create a person if quota is available
*
- * A new ID for the new person will be generated; and the ID field in the given newPerson will be ignored
+ * A new ID for the new person will be generated, and the ID field in the given newPerson will be ignored
*
* Consumes 1 API usage
*
* @param addressBookName
* @param newPerson
+ * @param previousETag
* @return a response wrapper, containing the added person if successful
*/
@Override
public RemoteResponse createPerson(String addressBookName, CloudPerson newPerson, String previousETag) {
logger.debug("createPerson called with: addressbook {}, person {}, prevETag {}", addressBookName, newPerson,
previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
CloudPerson returnedPerson = addPerson(fileData.getAllPersons(), newPerson);
- fileHandler.writeCloudAddressBookToFile(fileData);
-
- modifyCloudPersonBasedOnChance(returnedPerson);
-
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, returnedPerson,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(remoteResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
- return remoteResponse;
+ fileHandler.writeCloudAddressBook(fileData);
+ return new RemoteResponse(HttpURLConnection.HTTP_CREATED, returnedPerson, cloudRateLimitStatus,
+ previousETag);
} catch (IllegalArgumentException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -119,28 +98,23 @@ public RemoteResponse getPersons(String addressBookName, int pageNumber, int res
String previousETag) {
logger.debug("getPersons called with: addressbook {}, page {}, resourcesperpage {}, prevETag {}",
addressBookName, pageNumber, resourcesPerPage, previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
List fullPersonList = new ArrayList<>();
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
fullPersonList.addAll(fileData.getAllPersons());
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
List queryResults = getQueryResults(pageNumber, resourcesPerPage, fullPersonList);
-
- mutateCloudPersonList(queryResults);
-
RemoteResponse contentResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, queryResults,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(contentResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
+ cloudRateLimitStatus, previousETag);
- cloudRateLimitStatus.useQuota(1);
+ if (isNotModifiedResponse(contentResponse)) return contentResponse;
if (isValidPageNumber(fullPersonList.size(), pageNumber, resourcesPerPage)) {
fillInPageNumbers(pageNumber, resourcesPerPage, fullPersonList, contentResponse);
@@ -148,6 +122,10 @@ public RemoteResponse getPersons(String addressBookName, int pageNumber, int res
return contentResponse;
}
+ private boolean isNotModifiedResponse(RemoteResponse contentResponse) {
+ return contentResponse.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED;
+ }
+
/**
* Returns a response wrapper containing the list of tags if quota is available
*
@@ -156,34 +134,30 @@ public RemoteResponse getPersons(String addressBookName, int pageNumber, int res
* @param addressBookName
* @param pageNumber
* @param resourcesPerPage
+ * @param previousETag
* @return
*/
@Override
public RemoteResponse getTags(String addressBookName, int pageNumber, int resourcesPerPage, String previousETag) {
logger.debug("getTags called with: addressbook {}, page {}, resourcesperpage {}, prevETag {}", addressBookName,
pageNumber, resourcesPerPage, previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
List fullTagList = new ArrayList<>();
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
fullTagList.addAll(fileData.getAllTags());
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
List queryResults = getQueryResults(pageNumber, resourcesPerPage, fullTagList);
- modifyCloudTagListBasedOnChance(queryResults);
-
RemoteResponse contentResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, queryResults,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(contentResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
+ cloudRateLimitStatus, previousETag);
+ if (isNotModifiedResponse(contentResponse)) return contentResponse;
if (isValidPageNumber(fullTagList.size(), pageNumber, resourcesPerPage)) {
fillInPageNumbers(pageNumber, resourcesPerPage, fullTagList, contentResponse);
@@ -196,16 +170,14 @@ public RemoteResponse getTags(String addressBookName, int pageNumber, int resour
*
* This does NOT cost any API usage
*
+ * @param previousETag
* @return
*/
@Override
public RemoteResponse getRateLimitStatus(String previousETag) {
+ // TODO: Figure out GitHub response for limit status if ETag is provided
logger.debug("getRateLimitStatus called with: prevETag {}", previousETag);
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, getHeaders(cloudRateLimitStatus),
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(remoteResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
- return remoteResponse;
+ return RemoteResponse.getLimitStatusResponse(cloudRateLimitStatus);
}
/**
@@ -216,6 +188,7 @@ public RemoteResponse getRateLimitStatus(String previousETag) {
* @param addressBookName
* @param personId
* @param updatedPerson
+ * @param previousETag
* @return
*/
@Override
@@ -224,29 +197,18 @@ public RemoteResponse updatePerson(String addressBookName, int personId,
logger.debug("updatePerson called with: addressbook {}, personid {}, person {}, prevETag {}", addressBookName,
personId, updatedPerson, previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
CloudPerson resultingPerson = updatePersonDetails(fileData.getAllPersons(), fileData.getAllTags(), personId,
updatedPerson);
- fileHandler.writeCloudAddressBookToFile(fileData);
-
- modifyCloudPersonBasedOnChance(resultingPerson);
-
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, resultingPerson,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(remoteResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
- return remoteResponse;
+ fileHandler.writeCloudAddressBook(fileData);
+ return new RemoteResponse(HttpURLConnection.HTTP_OK, resultingPerson, cloudRateLimitStatus, previousETag);
} catch (NoSuchElementException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -263,21 +225,18 @@ public RemoteResponse updatePerson(String addressBookName, int personId,
@Override
public RemoteResponse deletePerson(String addressBookName, int personId) {
logger.debug("deletePerson called with: addressbook {}, personid {}", addressBookName, personId);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
deletePersonFromData(fileData.getAllPersons(), personId);
- fileHandler.writeCloudAddressBookToFile(fileData);
+ fileHandler.writeCloudAddressBook(fileData);
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_NO_CONTENT);
} catch (NoSuchElementException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -289,34 +248,24 @@ public RemoteResponse deletePerson(String addressBookName, int personId) {
*
* @param addressBookName
* @param newTag tag name should not already be used
+ * @param previousETag
* @return
*/
@Override
public RemoteResponse createTag(String addressBookName, CloudTag newTag, String previousETag) {
logger.debug("createTag called with: addressbook {}, tag {}, prevETag {}", addressBookName, newTag,
previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
CloudTag returnedTag = addTag(fileData.getAllTags(), newTag);
- fileHandler.writeCloudAddressBookToFile(fileData);
-
- modifyCloudTagBasedOnChance(returnedTag);
-
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, returnedTag,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(remoteResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
- return remoteResponse;
+ fileHandler.writeCloudAddressBook(fileData);
+ return new RemoteResponse(HttpURLConnection.HTTP_CREATED, returnedTag, cloudRateLimitStatus, previousETag);
} catch (IllegalArgumentException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -327,37 +276,27 @@ public RemoteResponse createTag(String addressBookName, CloudTag newTag, String
* Consumes 1 API usage
*
* @param addressBookName
- * @param oldTagName should match a existing tag's name
+ * @param oldTagName should match an existing tag's name
* @param updatedTag
+ * @param previousETag
* @return
*/
@Override
public RemoteResponse editTag(String addressBookName, String oldTagName, CloudTag updatedTag, String previousETag) {
logger.debug("editTag called with: addressbook {}, tagname {}, tag {}, prevETag {}", addressBookName,
oldTagName, updatedTag, previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
CloudTag returnedTag = updateTagDetails(fileData.getAllPersons(), fileData.getAllTags(), oldTagName,
updatedTag);
- fileHandler.writeCloudAddressBookToFile(fileData);
-
- modifyCloudTagBasedOnChance(returnedTag);
-
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, returnedTag,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(remoteResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
- return remoteResponse;
+ fileHandler.writeCloudAddressBook(fileData);
+ return new RemoteResponse(HttpURLConnection.HTTP_OK, returnedTag, cloudRateLimitStatus, previousETag);
} catch (NoSuchElementException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -375,21 +314,18 @@ public RemoteResponse editTag(String addressBookName, String oldTagName, CloudTa
@Override
public RemoteResponse deleteTag(String addressBookName, String tagName) {
logger.debug("deleteTag called with: addressbook {}, tagname {}", addressBookName, tagName);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
deleteTagFromData(fileData.getAllPersons(), fileData.getAllTags(), tagName);
- fileHandler.writeCloudAddressBookToFile(fileData);
+ fileHandler.writeCloudAddressBook(fileData);
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_NO_CONTENT);
} catch (NoSuchElementException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
}
@@ -405,21 +341,16 @@ public RemoteResponse deleteTag(String addressBookName, String tagName) {
@Override
public RemoteResponse createAddressBook(String addressBookName) {
logger.debug("createAddressBook called with: addressbook {}", addressBookName);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
-
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
try {
- fileHandler.createCloudAddressBookFile(addressBookName);
+ fileHandler.createAddressBook(addressBookName);
- cloudRateLimitStatus.useQuota(1);
//TODO: Return a wrapped simplified version of an empty addressbook (e.g. only fields such as name)
return getEmptyResponse(HttpURLConnection.HTTP_CREATED);
} catch (DataConversionException | IOException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
} catch (IllegalArgumentException e) {
- cloudRateLimitStatus.useQuota(1);
return getEmptyResponse(HttpURLConnection.HTTP_BAD_REQUEST);
}
}
@@ -427,10 +358,13 @@ public RemoteResponse createAddressBook(String addressBookName) {
/**
* Gets the list of persons that have been updated after a certain time, if quota is available
*
- * Consumes 1 + floor(updated person list/resourcesPerPage) API usage
+ * Consumes 1 API usage
*
* @param addressBookName
* @param timeString
+ * @param pageNumber
+ * @param resourcesPerPage
+ * @param previousETag
* @return
*/
@Override
@@ -438,32 +372,26 @@ public RemoteResponse getUpdatedPersons(String addressBookName, String timeStrin
int resourcesPerPage, String previousETag) {
logger.debug("getUpdatedPersons called with: addressbook {}, time {}, pageno {}, resourcesperpage {}, prevETag {}",
addressBookName, timeString, pageNumber, resourcesPerPage, previousETag);
- if (shouldSimulateNetworkFailure()) return getNetworkFailedResponse();
- if (shouldSimulateSlowResponse()) delayRandomAmount();
+ if (!hasApiQuotaRemaining()) return RemoteResponse.getForbiddenResponse(cloudRateLimitStatus);
List fullPersonList = new ArrayList<>();
try {
- CloudAddressBook fileData = fileHandler.readCloudAddressBookFromFile(addressBookName);
+ CloudAddressBook fileData = fileHandler.readCloudAddressBook(addressBookName);
fullPersonList.addAll(fileData.getAllPersons());
- } catch (FileNotFoundException | DataConversionException e) {
+ } catch (FileNotFoundException e) {
+ return getEmptyResponse(HttpURLConnection.HTTP_NOT_FOUND);
+ } catch (DataConversionException e) {
return getEmptyResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
}
- if (!hasApiQuotaRemaining()) return getEmptyResponse(HttpURLConnection.HTTP_FORBIDDEN);
-
LocalDateTime time = LocalDateTime.parse(timeString);
List filteredList = filterPersonsByTime(fullPersonList, time);
List queryResults = getQueryResults(pageNumber, resourcesPerPage, filteredList);
- mutateCloudPersonList(queryResults);
-
RemoteResponse contentResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, queryResults,
- getHeaders(cloudRateLimitStatus));
- String eTag = getResponseETag(contentResponse);
- if (eTag.equals(previousETag)) return getNotModifiedResponse();
-
- cloudRateLimitStatus.useQuota(1);
+ cloudRateLimitStatus, previousETag);
+ if (isNotModifiedResponse(contentResponse)) return contentResponse;
if (isValidPageNumber(filteredList.size(), pageNumber, resourcesPerPage)) {
fillInPageNumbers(pageNumber, resourcesPerPage, filteredList, contentResponse);
@@ -471,18 +399,6 @@ public RemoteResponse getUpdatedPersons(String addressBookName, String timeStrin
return contentResponse;
}
- private String getResponseETag(RemoteResponse response) {
- return response.getHeaders().get("ETag");
- }
-
- private HashMap getHeaders(CloudRateLimitStatus cloudRateLimitStatus) {
- HashMap headers = new HashMap<>();
- headers.put("X-RateLimit-Limit", String.valueOf(cloudRateLimitStatus.getQuotaLimit()));
- headers.put("X-RateLimit-Remaining", String.valueOf(cloudRateLimitStatus.getQuotaRemaining()));
- headers.put("X-RateLimit-Reset", String.valueOf(cloudRateLimitStatus.getQuotaReset()));
- return headers;
- }
-
/**
* Fills in the page index details for a cloud response
*
@@ -522,14 +438,9 @@ private List getQueryResults(int pageNumber, int resourcesPerPage, List filterPersonsByTime(List personList, LocalDateTime time) {
@@ -538,19 +449,6 @@ private List filterPersonsByTime(List personList, Loca
.collect(Collectors.toList());
}
- private boolean shouldSimulateNetworkFailure() {
- return shouldSimulateUnreliableNetwork && RANDOM_GENERATOR.nextDouble() <= FAILURE_PROBABILITY;
- }
-
- private boolean shouldSimulateSlowResponse() {
- return shouldSimulateUnreliableNetwork && RANDOM_GENERATOR.nextDouble() <= NETWORK_DELAY_PROBABILITY;
- }
-
- private RemoteResponse getNetworkFailedResponse() {
- logger.info("Simulated network failure occurred!");
- return new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
- }
-
private boolean hasApiQuotaRemaining() {
logger.info("Current quota left: {}", cloudRateLimitStatus.getQuotaRemaining());
return cloudRateLimitStatus.getQuotaRemaining() > 0;
@@ -581,10 +479,7 @@ private boolean isExistingTag(List tagList, CloudTag targetTag) {
private CloudPerson addPerson(List personList, CloudPerson newPerson)
throws IllegalArgumentException {
if (newPerson == null) throw new IllegalArgumentException("Person cannot be null");
- if (!newPerson.isValid()) {
- throw new IllegalArgumentException("Fields cannot be null");
- }
- if (isExistingPerson(personList, newPerson)) throw new IllegalArgumentException("Person already exists");
+ if (!newPerson.isValid()) throw new IllegalArgumentException("Invalid person");
CloudPerson personToAdd = generateIdForPerson(personList, newPerson);
personList.add(personToAdd);
@@ -593,7 +488,7 @@ private CloudPerson addPerson(List personList, CloudPerson newPerso
}
private CloudPerson generateIdForPerson(List personList, CloudPerson newPerson) {
- newPerson.setId(personList.size() + 1); // starts from one
+ newPerson.setId(personList.size() + 1);
return newPerson;
}
@@ -604,8 +499,7 @@ private Optional getPerson(List personList, int person
}
private CloudPerson updatePersonDetails(List personList, List tagList, int personId,
- CloudPerson updatedPerson)
- throws NoSuchElementException {
+ CloudPerson updatedPerson) throws NoSuchElementException {
CloudPerson oldPerson = getPersonIfExists(personList, personId);
oldPerson.updatedBy(updatedPerson);
@@ -624,72 +518,6 @@ private CloudPerson getPersonIfExists(List personList, int personId
return personQueryResult.get();
}
- private List mutateCloudPersonList(List CloudPersonList) {
- modifyCloudPersonList(CloudPersonList);
- addCloudPersonsBasedOnChance(CloudPersonList);
- return CloudPersonList;
- }
-
- private List mutateCloudTagList(List CloudTagList) {
- modifyCloudTagListBasedOnChance(CloudTagList);
- addCloudTagsBasedOnChance(CloudTagList);
- return CloudTagList;
- }
-
- private void modifyCloudPersonList(List cloudPersonList) {
- cloudPersonList.stream()
- .forEach(this::modifyCloudPersonBasedOnChance);
- }
-
- private void modifyCloudTagListBasedOnChance(List cloudTagList) {
- cloudTagList.stream()
- .forEach(this::modifyCloudTagBasedOnChance);
- }
-
- private void addCloudPersonsBasedOnChance(List personList) {
- for (int i = 0; i < MAX_NUM_PERSONS_TO_ADD; i++) {
- if (shouldSimulateUnreliableNetwork && RANDOM_GENERATOR.nextDouble() <= ADD_PERSON_PROBABILITY) {
- CloudPerson person = new CloudPerson(java.util.UUID.randomUUID().toString(),
- java.util.UUID.randomUUID().toString());
- logger.info("Cloud simulator: adding '{}'", person);
- personList.add(person);
- }
- }
- }
-
- private void addCloudTagsBasedOnChance(List tagList) {
- for (int i = 0; i < MAX_NUM_PERSONS_TO_ADD; i++) {
- if (shouldSimulateUnreliableNetwork && RANDOM_GENERATOR.nextDouble() <= ADD_TAG_PROBABILITY) {
- CloudTag tag = new CloudTag(java.util.UUID.randomUUID().toString());
- logger.debug("Cloud simulator: adding tag '{}'", tag);
- tagList.add(tag);
- }
- }
- }
-
- private void modifyCloudPersonBasedOnChance(CloudPerson cloudPerson) {
- if (!shouldSimulateUnreliableNetwork || RANDOM_GENERATOR.nextDouble() > MODIFY_PERSON_PROBABILITY) return;
- logger.debug("Cloud simulator: modifying person '{}'", cloudPerson);
- cloudPerson.setCity(java.util.UUID.randomUUID().toString());
- cloudPerson.setStreet(java.util.UUID.randomUUID().toString());
- cloudPerson.setPostalCode(String.valueOf(RANDOM_GENERATOR.nextInt(999999)));
- }
-
- private void modifyCloudTagBasedOnChance(CloudTag cloudTag) {
- if (!shouldSimulateUnreliableNetwork || RANDOM_GENERATOR.nextDouble() > MODIFY_TAG_PROBABILITY) return;
- logger.debug("Cloud simulator: modifying tag '{}'", cloudTag);
- cloudTag.setName(UUID.randomUUID().toString());
- }
-
- private void delayRandomAmount() {
- long delayAmount = RANDOM_GENERATOR.nextInt(DELAY_RANGE) + MIN_DELAY_IN_SEC;
- try {
- TimeUnit.SECONDS.sleep(delayAmount);
- } catch (InterruptedException e) {
- logger.warn("Error occurred while delaying cloud response.");
- }
- }
-
private boolean isValidPageNumber(int dataSize, int pageNumber, int resourcesPerPage) {
return pageNumber == 1 || getLastPageNumber(dataSize, resourcesPerPage) >= pageNumber;
}
@@ -706,7 +534,7 @@ private void deletePersonFromData(List personList, int personId)
private CloudTag addTag(List tagList, CloudTag newTag) {
if (newTag == null) throw new IllegalArgumentException("Tag cannot be null");
- if (!newTag.isValid()) throw new IllegalArgumentException("Fields cannot be null");
+ if (!newTag.isValid()) throw new IllegalArgumentException("Invalid tag");
if (isExistingTag(tagList, newTag)) throw new IllegalArgumentException("Tag already exists");
tagList.add(newTag);
return newTag;
@@ -726,8 +554,7 @@ private CloudTag getTagIfExists(List tagList, String tagName) {
}
private CloudTag updateTagDetails(List personList, List tagList, String oldTagName,
- CloudTag updatedTag)
- throws NoSuchElementException {
+ CloudTag updatedTag) throws NoSuchElementException {
CloudTag oldTag = getTagIfExists(tagList, oldTagName);
oldTag.updatedBy(updatedTag);
personList.stream()
diff --git a/src/main/java/address/sync/cloud/ICloudSimulator.java b/src/main/java/address/sync/cloud/IRemote.java
similarity index 97%
rename from src/main/java/address/sync/cloud/ICloudSimulator.java
rename to src/main/java/address/sync/cloud/IRemote.java
index 203aa6d9..65f2a239 100644
--- a/src/main/java/address/sync/cloud/ICloudSimulator.java
+++ b/src/main/java/address/sync/cloud/IRemote.java
@@ -3,7 +3,7 @@
import address.sync.cloud.model.CloudPerson;
import address.sync.cloud.model.CloudTag;
-public interface ICloudSimulator {
+public interface IRemote {
RemoteResponse createPerson(String addressBookName, CloudPerson newPerson, String previousETag);
RemoteResponse getPersons(String addressBookName, int pageNumber, int resourcesPerPage, String previousETag);
RemoteResponse getUpdatedPersons(String addressBookName, String timeString, int pageNumber, int resourcesPerPage, String previousETag);
diff --git a/src/main/java/address/sync/cloud/RemoteResponse.java b/src/main/java/address/sync/cloud/RemoteResponse.java
index e00a9695..ee4acbf4 100644
--- a/src/main/java/address/sync/cloud/RemoteResponse.java
+++ b/src/main/java/address/sync/cloud/RemoteResponse.java
@@ -16,6 +16,14 @@
import java.util.Formatter;
import java.util.HashMap;
+/**
+ * This class is meant to mimic the response of a GitHub request
+ *
+ * Construction of an object will use up an API quota in the given cloudRateLimitStatus if the previousETag is not
+ * provided or is found to be different to the given object's eTag
+ *
+ * RemoteResponse instances obtained via other means e.g. RemoteResponse.getForbiddenResponse should not use up quota
+ */
public class RemoteResponse {
private static final AppLogger logger = LoggerManager.getLogger(RemoteResponse.class);
@@ -27,6 +35,41 @@ public class RemoteResponse {
private int firstPageNo;
private int lastPageNo;
+ public RemoteResponse(int responseCode, Object body, CloudRateLimitStatus cloudRateLimitStatus, String previousETag) {
+ String newETag = getETag(convertToInputStream(body));
+
+ if (previousETag != null && previousETag.equals(newETag)) {
+ this.responseCode = HttpURLConnection.HTTP_NOT_MODIFIED;
+ this.headers = getRateLimitStatusHeader(cloudRateLimitStatus);
+ return;
+ }
+
+ cloudRateLimitStatus.useQuota(1);
+ this.responseCode = responseCode;
+ this.headers = getHeaders(cloudRateLimitStatus, newETag);
+ this.body = convertToInputStream(body);
+ }
+
+ private RemoteResponse(int responseCode, CloudRateLimitStatus cloudRateLimitStatus) {
+ this.responseCode = responseCode;
+ this.headers = getRateLimitStatusHeader(cloudRateLimitStatus);
+ this.body = convertToInputStream(getRateLimitStatusHeader(cloudRateLimitStatus));
+ }
+
+ private RemoteResponse(int responseCode, Object body, CloudRateLimitStatus cloudRateLimitStatus) {
+ this.responseCode = responseCode;
+ this.headers = getRateLimitStatusHeader(cloudRateLimitStatus);
+ this.body = convertToInputStream(body);
+ }
+
+ public static RemoteResponse getForbiddenResponse(CloudRateLimitStatus cloudRateLimitStatus) {
+ return new RemoteResponse(HttpURLConnection.HTTP_FORBIDDEN, null, cloudRateLimitStatus);
+ }
+
+ public static RemoteResponse getLimitStatusResponse(CloudRateLimitStatus cloudRateLimitStatus) {
+ return new RemoteResponse(HttpURLConnection.HTTP_OK, cloudRateLimitStatus);
+ }
+
public int getNextPageNo() {
return nextPageNo;
}
@@ -59,26 +102,6 @@ public void setLastPageNo(int lastPageNo) {
this.lastPageNo = lastPageNo;
}
- private void addETagToHeader(HashMap header, String eTag) {
- header.put("ETag", eTag);
- }
-
- public RemoteResponse(int responseCode, Object body, HashMap header) {
- this.responseCode = responseCode;
- if (body != null) {
- this.body = convertToInputStream(body);
- addETagToHeader(header, getETag(convertToInputStream(body)));
- }
- this.headers = header;
- }
-
- public RemoteResponse(int responseCode) {
- assert responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR : "RemoteResponse constructor misused";
- this.responseCode = responseCode;
- this.headers = new HashMap<>();
- }
-
-
public int getResponseCode() {
return responseCode;
}
@@ -94,13 +117,14 @@ public HashMap getHeaders() {
/**
* Calculates the hash of the input stream if it has content
*
- * The input stream will be digested. Caller should clone or
+ * WARNING: The input stream will be digested. Caller should clone or
* duplicate the stream before calling this method.
*
* @param bodyStream
* @return
*/
- public static String getETag(InputStream bodyStream) {
+ private String getETag(InputStream bodyStream) {
+ if (bodyStream == null) return null;
try {
// Adapted from http://www.javacreed.com/how-to-compute-hash-code-of-streams/
DigestInputStream digestInputStream = new DigestInputStream(new BufferedInputStream(bodyStream),
@@ -122,11 +146,30 @@ public static String getETag(InputStream bodyStream) {
}
}
- private static ByteArrayInputStream convertToInputStream(Object object) {
+ private void addETagToHeader(HashMap header, String eTag) {
+ header.put("ETag", eTag);
+ }
+
+ private HashMap getRateLimitStatusHeader(CloudRateLimitStatus cloudRateLimitStatus) {
+ HashMap headers = new HashMap<>();
+ headers.put("X-RateLimit-Limit", String.valueOf(cloudRateLimitStatus.getQuotaLimit()));
+ headers.put("X-RateLimit-Remaining", String.valueOf(cloudRateLimitStatus.getQuotaRemaining()));
+ headers.put("X-RateLimit-Reset", String.valueOf(cloudRateLimitStatus.getQuotaReset()));
+ return headers;
+ }
+
+ private HashMap getHeaders(CloudRateLimitStatus cloudRateLimitStatus, String eTag) {
+ HashMap headers = getRateLimitStatusHeader(cloudRateLimitStatus);
+ addETagToHeader(headers, eTag);
+ return headers;
+ }
+
+ private ByteArrayInputStream convertToInputStream(Object object) {
+ if (object == null) return null;
try {
return new ByteArrayInputStream(JsonUtil.toJsonString(object).getBytes());
} catch (JsonProcessingException e) {
- e.printStackTrace();
+ logger.warn("Error converting object {} to input stream", object);
return null;
}
}
diff --git a/src/main/java/address/sync/cloud/model/CloudPerson.java b/src/main/java/address/sync/cloud/model/CloudPerson.java
index 528e8a72..d65d05a0 100644
--- a/src/main/java/address/sync/cloud/model/CloudPerson.java
+++ b/src/main/java/address/sync/cloud/model/CloudPerson.java
@@ -27,15 +27,32 @@ public class CloudPerson {
private LocalDate birthday;
public CloudPerson() {
+ this.id = 0;
+ this.tags = new ArrayList<>();
+ this.firstName = "";
+ this.lastName = "";
+ this.street = "";
+ this.city = "";
+ this.postalCode = "";
+ this.isDeleted = false;
}
public CloudPerson(String firstName, String lastName) {
+ this();
this.firstName = firstName;
this.lastName = lastName;
- this.tags = new ArrayList<>();
setLastUpdatedAt(LocalDateTime.now());
}
+ public CloudPerson(String firstName, String lastName, int id) {
+ this(firstName, lastName);
+ setId(id);
+ }
+
+ public CloudPerson(CloudPerson cloudPerson) {
+ updatedBy(cloudPerson);
+ }
+
public int getId() {
return id;
}
diff --git a/src/main/java/address/sync/cloud/model/CloudTag.java b/src/main/java/address/sync/cloud/model/CloudTag.java
index 3ce41b35..4107b3ba 100644
--- a/src/main/java/address/sync/cloud/model/CloudTag.java
+++ b/src/main/java/address/sync/cloud/model/CloudTag.java
@@ -22,6 +22,10 @@ public CloudTag(String name) {
setLastUpdatedAt(LocalDateTime.now());
}
+ public CloudTag(CloudTag cloudTag) {
+ updatedBy(cloudTag);
+ }
+
@XmlElement(name = "name")
public String getName() {
return name;
diff --git a/src/main/java/address/sync/task/GetUpdatesFromRemoteTask.java b/src/main/java/address/sync/task/GetUpdatesFromRemoteTask.java
index 5dffb62c..2f67e67a 100644
--- a/src/main/java/address/sync/task/GetUpdatesFromRemoteTask.java
+++ b/src/main/java/address/sync/task/GetUpdatesFromRemoteTask.java
@@ -38,9 +38,9 @@ public void run() {
}
try {
List updatedPersons = getUpdatedPersons(syncActiveAddressBookName.get());
- logger.logList("Found updated persons: {}", updatedPersons);
- List latestTags = getLatestTags(syncActiveAddressBookName.get());
- logger.logList("Found latest tags: {}", latestTags);
+ logger.debug("Updated persons: {}", updatedPersons);
+ Optional> latestTags = getLatestTags(syncActiveAddressBookName.get());
+ logger.debug("Latest tags: {}", latestTags);
eventRaiser.accept(new SyncCompletedEvent(updatedPersons, latestTags));
} catch (SyncErrorException e) {
@@ -61,12 +61,8 @@ public void run() {
*/
private List getUpdatedPersons(String addressBookName) throws SyncErrorException {
try {
- Optional> updatedPersons;
- updatedPersons = remoteManager.getUpdatedPersons(addressBookName);
-
+ Optional> updatedPersons = remoteManager.getUpdatedPersons(addressBookName);
if (!updatedPersons.isPresent()) throw new SyncErrorException("getUpdatedPersons failed.");
-
- logger.logList("Updated persons: {}", updatedPersons.get());
return updatedPersons.get();
} catch (IOException e) {
throw new SyncErrorException("Error getting updated persons.");
@@ -80,17 +76,9 @@ private List getUpdatedPersons(String addressBookName) throws SyncErrorE
* @return
* @throws SyncErrorException if bad response code, missing data or network error
*/
- private List getLatestTags(String addressBookName) throws SyncErrorException {
+ private Optional> getLatestTags(String addressBookName) throws SyncErrorException {
try {
- Optional> latestTags = remoteManager.getLatestTagList(addressBookName);
-
- if (!latestTags.isPresent()) {
- logger.info("No updates to tags.");
- return null;
- } else {
- logger.logList("Latest tags: {}", latestTags.get());
- return latestTags.get();
- }
+ return remoteManager.getLatestTagList(addressBookName);
} catch (IOException e) {
throw new SyncErrorException("Error getting latest tags.");
}
diff --git a/src/main/java/address/ui/CommandInfoListViewCell.java b/src/main/java/address/ui/CommandInfoListViewCell.java
new file mode 100644
index 00000000..74e22f6e
--- /dev/null
+++ b/src/main/java/address/ui/CommandInfoListViewCell.java
@@ -0,0 +1,22 @@
+package address.ui;
+
+import address.controller.ActivityHistoryCardController;
+import address.model.CommandInfo;
+import javafx.scene.control.ListCell;
+
+/**
+ *
+ */
+public class CommandInfoListViewCell extends ListCell {
+
+ @Override
+ protected void updateItem(CommandInfo item, boolean empty) {
+ super.updateItem(item, empty);
+ if (item == null || empty) {
+ setGraphic(null);
+ setText("");
+ } else {
+ setGraphic(new ActivityHistoryCardController(item).getLayout());
+ }
+ }
+}
diff --git a/src/main/java/address/ui/Ui.java b/src/main/java/address/ui/Ui.java
index 9bd363df..a300a3d6 100644
--- a/src/main/java/address/ui/Ui.java
+++ b/src/main/java/address/ui/Ui.java
@@ -1,10 +1,11 @@
package address.ui;
import address.MainApp;
-import address.browser.BrowserManager;
import address.controller.MainController;
import address.model.ModelManager;
+import address.model.UserPrefs;
import address.util.Config;
+import address.util.GuiSettings;
import javafx.scene.control.Alert;
import javafx.stage.Stage;
@@ -13,9 +14,11 @@
*/
public class Ui {
MainController mainController;
+ UserPrefs pref;
- public Ui(MainApp mainApp, ModelManager modelManager, Config config){
- mainController = new MainController(mainApp, modelManager, config);
+ public Ui(MainApp mainApp, ModelManager modelManager, Config config, UserPrefs pref){
+ mainController = new MainController(mainApp, modelManager, config, pref);
+ this.pref = pref;
}
public void start(Stage primaryStage) {
@@ -27,6 +30,10 @@ public void showAlertDialogAndWait(Alert.AlertType alertType, String alertTitle,
}
public void stop() {
+ Stage stage = mainController.getPrimaryStage();
+ GuiSettings guiSettings = new GuiSettings(stage.getWidth(), stage.getHeight(),
+ (int)stage.getX(), (int)stage.getY());
+ pref.setGuiSettings(guiSettings);
mainController.stop();
}
}
diff --git a/src/main/java/address/updater/BackupHandler.java b/src/main/java/address/updater/BackupHandler.java
index c87ee842..0196d4c5 100644
--- a/src/main/java/address/updater/BackupHandler.java
+++ b/src/main/java/address/updater/BackupHandler.java
@@ -8,8 +8,11 @@
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URISyntaxException;
+import java.nio.file.Files;
import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -21,10 +24,13 @@
public class BackupHandler {
private static final AppLogger logger = LoggerManager.getLogger(BackupHandler.class);
private static final int MAX_BACKUP_JAR_KEPT = 3;
+ private static final String BACKUP_DIR = "past_versions";
private static final String BACKUP_MARKER = "_";
private static final String BACKUP_FILENAME_STRING_FORMAT = "addressbook" + BACKUP_MARKER + "%s.jar";
private static final String BACKUP_FILENAME_PATTERN_STRING =
"addressbook" + BACKUP_MARKER + "(" + Version.VERSION_PATTERN_STRING + ")\\.(jar|JAR)$";
+ private static final String BACKUP_INSTRUCTION_FILENAME = "Instruction to use past versions.txt";
+ private static final String BACKUP_INSTRUCTION_RESOURCE_PATH = "updater/Instruction to use past versions.txt";
private final Version currentVersion;
private DependencyHistoryHandler dependencyHistoryHandler;
@@ -47,16 +53,49 @@ public void createBackupOfApp(Version version) throws IOException, URISyntaxExce
return;
}
+ try {
+ createBackupDirIfMissing();
+ } catch (IOException e) {
+ logger.debug("Failed to create backup directory", e);
+ throw e;
+ }
+
+ extractInstructionToUseBackupVersion();
+
String backupFilename = getBackupFilename(version);
try {
- FileUtil.copyFile(mainAppJar.toPath(), Paths.get(backupFilename), true);
+ FileUtil.copyFile(mainAppJar.toPath(), Paths.get(BACKUP_DIR, backupFilename), true);
} catch (IOException e) {
logger.debug("Failed to create backup", e);
throw e;
}
}
+ private void createBackupDirIfMissing() throws IOException {
+ File backupDir = new File(BACKUP_DIR);
+
+ if (!FileUtil.isDirExists(backupDir)) {
+ FileUtil.createDirs(new File(BACKUP_DIR));
+ }
+ }
+
+ private void extractInstructionToUseBackupVersion() throws IOException {
+ File backupInstructionFile = new File(BACKUP_INSTRUCTION_FILENAME);
+
+ if (!backupInstructionFile.exists() && !backupInstructionFile.createNewFile()) {
+ throw new IOException("Failed to create backup instruction empty file");
+ }
+
+ try (InputStream in =
+ BackupHandler.class.getClassLoader().getResourceAsStream(BACKUP_INSTRUCTION_RESOURCE_PATH)) {
+ Files.copy(in, backupInstructionFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ logger.debug("Failed to extract backup instruction");
+ throw e;
+ }
+ }
+
private boolean isRunFromBackupJar(String jarName) {
return jarName.contains(BACKUP_MARKER);
}
@@ -69,6 +108,7 @@ private String getBackupFilename(Version version) {
* Assumes user won't change backup filenames
*/
public void cleanupBackups() {
+ logger.debug("Cleaning backups");
if (dependencyHistoryHandler.getCurrentVersionDependencies() == null ||
dependencyHistoryHandler.getCurrentVersionDependencies().isEmpty()) {
@@ -85,7 +125,7 @@ public void cleanupBackups() {
logger.debug("Deleting {}", allBackupFilenames.get(i));
try {
- FileUtil.deleteFile(allBackupFilenames.get(i));
+ FileUtil.deleteFile(BACKUP_DIR + File.separator + allBackupFilenames.get(i));
deletedVersions.add(getVersionOfBackupFileFromFilename(allBackupFilenames.get(i)));
} catch (IOException e) {
logger.warn("Failed to delete old backup file: {}", e);
@@ -123,20 +163,24 @@ public void cleanupBackups() {
* @return all backup filenames aside from current app sorted from lowest version to latest
*/
private List getAllBackupFilenamesAsideFromCurrent() {
- File currDirectory = new File(".");
+ File backupDir = new File(BACKUP_DIR);
- File[] filesInCurrentDirectory = currDirectory.listFiles();
+ if (!FileUtil.isDirExists(backupDir)) {
+ logger.debug("No backup directory");
+ return new ArrayList<>();
+ }
- if (filesInCurrentDirectory == null) {
- // current directory always exists
- assert false;
+ File[] backupFiles = backupDir.listFiles();
+
+ if (backupFiles == null) {
+ logger.debug("No backup files found");
return new ArrayList<>();
}
- List listOfFilesInCurrDirectory = new ArrayList<>(Arrays.asList(filesInCurrentDirectory));
+ List listOfBackupFiles = new ArrayList<>(Arrays.asList(backupFiles));
// Exclude current version in case user is running backup Jar
- return listOfFilesInCurrDirectory.stream()
+ return listOfBackupFiles.stream()
.filter(f ->
!f.getName().equals(getBackupFilename(currentVersion))
&& f.getName().matches(BACKUP_FILENAME_PATTERN_STRING))
diff --git a/src/main/java/address/updater/UpdateManager.java b/src/main/java/address/updater/UpdateManager.java
index ae2279c9..d5016ede 100644
--- a/src/main/java/address/updater/UpdateManager.java
+++ b/src/main/java/address/updater/UpdateManager.java
@@ -53,6 +53,8 @@ public class UpdateManager extends ComponentManager {
private static final String VERSION_DESCRIPTOR_ON_SERVER_EARLY =
"https://raw.githubusercontent.com/HubTurbo/addressbook/early-access/UpdateData.json";
private static final File VERSION_DESCRIPTOR_FILE = new File(UPDATE_DIR + File.separator + "UpdateData.json");
+ private static final String LIB_DIR = "lib" + File.separator;
+ private static final String MAIN_APP_FILEPATH = LIB_DIR + "resource.jar";
private final ExecutorService pool = Executors.newCachedThreadPool();
private final DependencyHistoryHandler dependencyHistoryHandler;
@@ -161,7 +163,7 @@ private void checkForUpdate() {
raise(new UpdaterFinishedEvent("Update will be applied on next launch"));
this.isUpdateApplicable = true;
- updateDownloadedVersionsData(latestVersion.get());
+ downloadedVersions.add(latestVersion.get());
}
/**
@@ -227,12 +229,13 @@ private HashMap collectAllUpdateFilesToBeDownloaded(VersionDescript
mainAppDownloadLink = versionDescriptor.getDownloadLinkForMainApp();
- filesToBeDownloaded.put("addressbook.jar", mainAppDownloadLink);
+ filesToBeDownloaded.put(MAIN_APP_FILEPATH, mainAppDownloadLink);
versionDescriptor.getLibraries().stream()
.filter(libDesc -> libDesc.getOs() == OsDetector.Os.ANY || libDesc.getOs() == OsDetector.getOs())
- .filter(libDesc -> !FileUtil.isFileExists("lib/" + libDesc.getFilename()))
- .forEach(libDesc -> filesToBeDownloaded.put("lib/" + libDesc.getFilename(), libDesc.getDownloadLink()));
+ .filter(libDesc -> !FileUtil.isFileExists(LIB_DIR + libDesc.getFilename()))
+ .forEach(libDesc -> filesToBeDownloaded.put(LIB_DIR + libDesc.getFilename(),
+ libDesc.getDownloadLink()));
return filesToBeDownloaded;
}
@@ -299,11 +302,6 @@ private void extractJarUpdater() throws IOException {
}
}
- private void updateDownloadedVersionsData(Version latestVersionDownloaded) {
- downloadedVersions.add(latestVersionDownloaded);
- writeDownloadedVersionsToFile();
- }
-
private void writeDownloadedVersionsToFile() {
try {
if (FileUtil.isFileExists(DOWNLOADED_VERSIONS_FILE.toString())) {
@@ -345,6 +343,8 @@ public void applyUpdate() {
return;
}
+ writeDownloadedVersionsToFile();
+
String restarterAppPath = JAR_UPDATER_APP_PATH;
String localUpdateSpecFilepath = LocalUpdateSpecificationHelper.getLocalUpdateSpecFilepath();
String cmdArg = String.format("--update-specification=%s --source-dir=%s", localUpdateSpecFilepath, UPDATE_DIR);
diff --git a/src/main/java/address/util/Config.java b/src/main/java/address/util/Config.java
index c0991dd7..fe99e51e 100644
--- a/src/main/java/address/util/Config.java
+++ b/src/main/java/address/util/Config.java
@@ -5,50 +5,53 @@
import java.io.File;
import java.util.HashMap;
+import java.util.Optional;
/**
* Config values used by the app
*/
public class Config {
- private static final String CONFIG_FILE = "config.ini";
-
// Default values
private static final long DEFAULT_UPDATE_INTERVAL = 10000;
private static final Level DEFAULT_LOGGING_LEVEL = Level.INFO;
- private static final boolean DEFAULT_NETWORK_UNRELIABLE_MODE = false;
private static final HashMap DEFAULT_SPECIAL_LOG_LEVELS = new HashMap<>();
private static final int DEFAULT_BROWSER_NO_OF_PAGES = 3;
private static final BrowserType DEFAULT_BROWSER_TYPE = BrowserType.FULL_FEATURE_BROWSER;
+ private static final String DEFAULT_LOCAL_DATA_FILE_PATH = "test.xml";
+ private static final String DEFAULT_CLOUD_DATA_FILE_PATH = null; // For use in CloudManipulator for manual testing
+ private static final String DEFAULT_ADDRESS_BOOK_NAME = "MyAddressBook";
// Config values
- public String appTitle = "Address App";
+ private String appTitle = "Address App";
// Customizable through config file
- public long updateInterval = DEFAULT_UPDATE_INTERVAL;
- public boolean simulateUnreliableNetwork = DEFAULT_NETWORK_UNRELIABLE_MODE;
+ private long updateInterval = DEFAULT_UPDATE_INTERVAL;
public Level currentLogLevel = DEFAULT_LOGGING_LEVEL;
public HashMap specialLogLevels = DEFAULT_SPECIAL_LOG_LEVELS;
private File prefsFileLocation = new File("preferences.json"); //Default user preferences file
public int browserNoOfPages = DEFAULT_BROWSER_NO_OF_PAGES;
public BrowserType browserType = DEFAULT_BROWSER_TYPE;
+ private String localDataFilePath = DEFAULT_LOCAL_DATA_FILE_PATH;
+ private String cloudDataFilePath = DEFAULT_CLOUD_DATA_FILE_PATH;
+ private String addressBookName = DEFAULT_ADDRESS_BOOK_NAME;
public Config() {
}
- public long getUpdateInterval() {
- return updateInterval;
+ public String getAppTitle() {
+ return appTitle;
}
- public void setUpdateInterval(long updateInterval) {
- this.updateInterval = updateInterval;
+ public void setAppTitle(String appTitle) {
+ this.appTitle = appTitle;
}
- public boolean isSimulateUnreliableNetwork() {
- return simulateUnreliableNetwork;
+ public long getUpdateInterval() {
+ return updateInterval;
}
- public void setSimulateUnreliableNetwork(boolean simulateUnreliableNetwork) {
- this.simulateUnreliableNetwork = simulateUnreliableNetwork;
+ public void setUpdateInterval(long updateInterval) {
+ this.updateInterval = updateInterval;
}
public Level getCurrentLogLevel() {
@@ -86,4 +89,30 @@ public BrowserType getBrowserType() {
public void setBrowserType(BrowserType browserType) {
this.browserType = browserType;
}
+
+ public String getLocalDataFilePath() {
+ return localDataFilePath;
+ }
+
+ public void setLocalDataFilePath(String localDataFilePath) {
+ this.localDataFilePath = localDataFilePath;
+ }
+
+ public String getCloudDataFilePath() {
+ return cloudDataFilePath;
+ }
+
+ public void setCloudDataFilePath(String cloudDataFilePath) {
+ this.cloudDataFilePath = cloudDataFilePath;
+ }
+
+ public String getAddressBookName() {
+ return addressBookName;
+ }
+
+ public void setAddressBookName(String addressBookName) {
+ this.addressBookName = addressBookName;
+ }
+
+
}
diff --git a/src/main/java/address/util/FileUtil.java b/src/main/java/address/util/FileUtil.java
index d5c730bc..4c468c06 100644
--- a/src/main/java/address/util/FileUtil.java
+++ b/src/main/java/address/util/FileUtil.java
@@ -192,4 +192,16 @@ public static String getPath(String pathWithForwardSlash) {
assert pathWithForwardSlash.contains("/");
return pathWithForwardSlash.replace("/", File.separator);
}
+
+ /**
+ * Gets the file name from the given file path, assuming that
+ * path components are '/'-separated
+ *
+ * @param filePath should not be null
+ * @return
+ */
+ public static String getFileName(String filePath) {
+ String[] pathComponents = filePath.split("/");
+ return pathComponents[pathComponents.length - 1];
+ }
}
diff --git a/src/main/java/address/util/GuiSettings.java b/src/main/java/address/util/GuiSettings.java
new file mode 100644
index 00000000..6d153003
--- /dev/null
+++ b/src/main/java/address/util/GuiSettings.java
@@ -0,0 +1,41 @@
+package address.util;
+
+import java.awt.*;
+import java.io.Serializable;
+
+/**
+ * A Serializable class that contains the GUI settings.
+ */
+public class GuiSettings implements Serializable {
+
+ public static final double DEFAULT_HEIGHT = 600;
+ public static final double DEFAULT_WIDTH = 740;
+
+ private Double windowWidth;
+ private Double windowHeight;
+ private Point windowCoordinates;
+
+ public GuiSettings() {
+ this.windowWidth = DEFAULT_WIDTH;
+ this.windowHeight = DEFAULT_HEIGHT;
+ this.windowCoordinates = null; // null represent no coordinates
+ }
+
+ public GuiSettings(Double windowWidth, Double windowHeight, int xPosition, int yPosition) {
+ this.windowWidth = windowWidth;
+ this.windowHeight = windowHeight;
+ this.windowCoordinates = new Point(xPosition, yPosition);
+ }
+
+ public Double getWindowWidth() {
+ return windowWidth;
+ }
+
+ public Double getWindowHeight() {
+ return windowHeight;
+ }
+
+ public Point getWindowCoordinates() {
+ return windowCoordinates;
+ }
+}
diff --git a/src/main/java/address/util/PlatformExecUtil.java b/src/main/java/address/util/PlatformExecUtil.java
index 247aa014..ec7eddb1 100644
--- a/src/main/java/address/util/PlatformExecUtil.java
+++ b/src/main/java/address/util/PlatformExecUtil.java
@@ -23,7 +23,7 @@ public static void runLater(Runnable action) {
*/
public static Future call(Callable callback) {
final FutureTask task = new FutureTask<>(callback);
- if (Platform.isFxApplicationThread()) {
+ if (isFxThread()) {
task.run();
} else {
runLater(task);
@@ -92,13 +92,17 @@ public static void runLaterAndWait(Runnable action) {
*/
public static void runAndWait(Runnable action) {
assert action != null : "Non-null action required";
- if (Platform.isFxApplicationThread()) {
+ if (isFxThread()) {
action.run();
return;
}
runLaterAndWait(action);
}
+ public static boolean isFxThread() {
+ return Platform.isFxApplicationThread();
+ }
+
private PlatformExecUtil() {
}
}
diff --git a/src/main/java/hubturbo/embeddedbrowser/HyperBrowser.java b/src/main/java/hubturbo/embeddedbrowser/HyperBrowser.java
index 8a744514..d18ff5b9 100644
--- a/src/main/java/hubturbo/embeddedbrowser/HyperBrowser.java
+++ b/src/main/java/hubturbo/embeddedbrowser/HyperBrowser.java
@@ -55,7 +55,7 @@ public HyperBrowser(BrowserType browserType, int noOfPages, Optional initi
private void initialiseHyperBrowser(){
this.hyperBrowserView = new AnchorPane();
-
+ FxViewUtil.applyAnchorBoundaryParameters(hyperBrowserView, 0.0, 0.0, 0.0, 0.0);
if (initialScreen.isPresent()) {
hyperBrowserView.getChildren().add(initialScreen.get());
}
diff --git a/src/main/java/hubturbo/updater/Installer.java b/src/main/java/hubturbo/updater/Installer.java
index 7e7eea6e..27fbe8ea 100644
--- a/src/main/java/hubturbo/updater/Installer.java
+++ b/src/main/java/hubturbo/updater/Installer.java
@@ -44,6 +44,7 @@ public class Installer extends Application {
private static final String ERROR_RUNNING = "Failed to run application";
private static final String ERROR_TRY_AGAIN = "Please try again, or contact developer if it keeps failing.";
private static final String LIB_DIR = "lib";
+ private static final Path MAIN_APP_FILEPATH = Paths.get(LIB_DIR, new File("resource.jar").getName());
private final ExecutorService pool = Executors.newSingleThreadExecutor();
private ProgressBar progressBar;
@@ -139,6 +140,17 @@ private void unpackAllJarsInsideSelf() throws IOException, URISyntaxException {
String filename = jarEntry.getName();
Path extractDest = Paths.get(LIB_DIR, new File(filename).getName());
+ // For MainApp resource, only extract if it is not present
+ if (filename.startsWith("resource") && filename.endsWith(".jar")) {
+ if (!MAIN_APP_FILEPATH.toFile().exists()) {
+ try (InputStream in = jar.getInputStream(jarEntry)) {
+ Files.copy(in, MAIN_APP_FILEPATH, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ continue;
+ }
+
+ // For other JARs, extract if existing files are of different sizes
if (filename.endsWith(".jar") && jarEntry.getSize() != extractDest.toFile().length()) {
try (InputStream in = jar.getInputStream(jarEntry)) {
Files.copy(in, extractDest, StandardCopyOption.REPLACE_EXISTING);
@@ -146,7 +158,7 @@ private void unpackAllJarsInsideSelf() throws IOException, URISyntaxException {
}
}
} catch (IOException e) {
- System.out.println("Failed to extract jar updater");
+ System.out.println("Failed to extract libraries");
throw e;
}
diff --git a/src/main/java/hubturbo/updater/UpdateDataGenerator.java b/src/main/java/hubturbo/updater/UpdateDataGenerator.java
index 120d782e..eb668035 100644
--- a/src/main/java/hubturbo/updater/UpdateDataGenerator.java
+++ b/src/main/java/hubturbo/updater/UpdateDataGenerator.java
@@ -6,11 +6,11 @@
import address.updater.VersionDescriptor;
import address.util.FileUtil;
import address.util.JsonUtil;
-import address.util.OsDetector;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
+import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -20,8 +20,8 @@
* Used to help developer create release in generating the update data
*/
public class UpdateDataGenerator {
- private static final String MAIN_APP_BASE_DOWNLOAD_LINK =
- "https://github.com/HubTurbo/addressbook/releases/download/";
+ private static final String BASE_DOWNLOAD_LINK =
+ "https://github.com/HubTurbo/addressbook/releases/download/Resources/";
private static final File UPDATE_DATA_FILE = new File("UpdateData.json");
public static void main(String[] args) {
@@ -49,12 +49,15 @@ public static void main(String[] args) {
}
ArrayList currentLibrariesName = new ArrayList<>(arguments.subList(1, arguments.size()));
+
ArrayList currentLibraryDescriptors = currentLibrariesName.stream()
- .map(libName -> new LibraryDescriptor(libName, null, OsDetector.Os.ANY))
+ .map(libName -> new LibraryDescriptor(libName, null, null))
.collect(Collectors.toCollection(ArrayList::new));
- populateCurrLibDescriptorWithExistingDownloadLink(previousVersionDescriptor.getLibraries(),
- currentLibraryDescriptors);
+ populateCurrLibDescriptorDownloadLink(currentLibraryDescriptors);
+
+ populateCurrLibDescriptorWithExistingLibDescriptorOs(previousVersionDescriptor.getLibraries(),
+ currentLibraryDescriptors);
versionDescriptor.setLibraries(currentLibraryDescriptors);
@@ -75,13 +78,28 @@ private static VersionDescriptor getPreviousUpdateData() throws IOException {
private static void setUpdateDataMainAppDownloadLink(VersionDescriptor versionDescriptor, String mainAppFilename)
throws MalformedURLException {
- String mainAppDownloadLinkString = MAIN_APP_BASE_DOWNLOAD_LINK + MainApp.VERSION.toString() + "/" +
- mainAppFilename;
+ String mainAppDownloadLinkString = BASE_DOWNLOAD_LINK + mainAppFilename;
versionDescriptor.setMainAppDownloadLink(mainAppDownloadLinkString);
}
- private static void populateCurrLibDescriptorWithExistingDownloadLink(
+ private static URL getDownloadLinkForLib(String libFilename) throws MalformedURLException {
+ return new URL(BASE_DOWNLOAD_LINK + libFilename);
+ }
+
+ private static void populateCurrLibDescriptorDownloadLink(ArrayList currentLibraryDescriptors) {
+ currentLibraryDescriptors.stream().forEach(libDesc -> {
+ try {
+ libDesc.setDownloadLink(getDownloadLinkForLib(libDesc.getFilename()));
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ System.out.println("Failed to set download link for " + libDesc.getFilename() +
+ "; please update the download link manually");
+ }
+ });
+ }
+
+ private static void populateCurrLibDescriptorWithExistingLibDescriptorOs(
ArrayList previousLibraryDescriptors,
ArrayList currentLibraryDescriptors) {
currentLibraryDescriptors.stream()
@@ -89,16 +107,13 @@ private static void populateCurrLibDescriptorWithExistingDownloadLink(
previousLibraryDescriptors.stream()
.filter(prevLibDesc -> prevLibDesc.getFilename().equals(libDesc.getFilename()))
.findFirst()
- .ifPresent(prevLibDesc -> {
- libDesc.setDownloadLink(prevLibDesc.getDownloadLink());
- libDesc.setOs(prevLibDesc.getOs());
- }));
+ .ifPresent(prevLibDesc -> libDesc.setOs(prevLibDesc.getOs())));
}
private static void notifyOfNewLibrariesToBeGivenMoreInformation(ArrayList libraryDescriptors) {
System.out.println("------------------------------------------------------------");
System.out.println("New libraries to be uploaded, given download URL and set OS:");
- libraryDescriptors.stream().filter(libDesc -> libDesc.getDownloadLink() == null)
+ libraryDescriptors.stream().filter(libDesc -> libDesc.getOs() == null)
.forEach(libDesc -> System.out.println(libDesc.getFilename()));
System.out.println("------------------------------------------------------------");
}
diff --git a/src/main/resources/images/clock.png b/src/main/resources/images/clock.png
new file mode 100644
index 00000000..0807cbf6
Binary files /dev/null and b/src/main/resources/images/clock.png differ
diff --git a/src/main/resources/images/fail.png b/src/main/resources/images/fail.png
new file mode 100644
index 00000000..6daf0129
Binary files /dev/null and b/src/main/resources/images/fail.png differ
diff --git a/src/main/resources/images/help_icon.png b/src/main/resources/images/help_icon.png
new file mode 100644
index 00000000..f8e80d6c
Binary files /dev/null and b/src/main/resources/images/help_icon.png differ
diff --git a/src/main/resources/images/info_icon.png b/src/main/resources/images/info_icon.png
new file mode 100644
index 00000000..f8cef714
Binary files /dev/null and b/src/main/resources/images/info_icon.png differ
diff --git a/src/main/resources/images/sync_icon.png b/src/main/resources/images/sync_icon.png
deleted file mode 100644
index 4d69b8f6..00000000
Binary files a/src/main/resources/images/sync_icon.png and /dev/null differ
diff --git a/src/main/resources/updater/Instruction to use past versions.txt b/src/main/resources/updater/Instruction to use past versions.txt
new file mode 100644
index 00000000..70c0e07d
--- /dev/null
+++ b/src/main/resources/updater/Instruction to use past versions.txt
@@ -0,0 +1 @@
+If the latest version does not work for some reason, you may want to use a past version until the latest version is fixed. In such a case, you can copy the JAR file of the version you want to use from the past_versions folder to the root folder (no need to rename it) and run that JAR file.
\ No newline at end of file
diff --git a/src/main/resources/view/ActivityHistory.fxml b/src/main/resources/view/ActivityHistory.fxml
new file mode 100644
index 00000000..13d2d654
--- /dev/null
+++ b/src/main/resources/view/ActivityHistory.fxml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/ActivityHistoryCard.fxml b/src/main/resources/view/ActivityHistoryCard.fxml
new file mode 100644
index 00000000..bfbf1acb
--- /dev/null
+++ b/src/main/resources/view/ActivityHistoryCard.fxml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css
index 9bc57cc8..465e4fe6 100644
--- a/src/main/resources/view/DarkTheme.css
+++ b/src/main/resources/view/DarkTheme.css
@@ -245,6 +245,14 @@
#pendingStateLabel {
-fx-font-size: 11px;
-fx-text-fill: #F70D1A;
+}
+
+#pendingCountdownIndicator {
+ -fx-font-size: 11px;
+ -fx-text-fill: #F70D1A;
+}
+
+#pendingStateHolder {
-fx-background-radius: 5;
-fx-background-color: white;
-fx-padding-background-color: transparent;
@@ -252,3 +260,7 @@
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0);
}
+#activityLabel {
+ -fx-font-size: 12px;
+}
+
diff --git a/src/main/resources/view/Help.fxml b/src/main/resources/view/Help.fxml
new file mode 100644
index 00000000..3853f03d
--- /dev/null
+++ b/src/main/resources/view/Help.fxml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml
index 23742a06..2c119bc9 100644
--- a/src/main/resources/view/PersonListCard.fxml
+++ b/src/main/resources/view/PersonListCard.fxml
@@ -1,5 +1,6 @@
+
@@ -9,13 +10,13 @@
-
+
-
+
@@ -27,7 +28,6 @@
-
@@ -35,17 +35,22 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/PersonOverview.fxml b/src/main/resources/view/PersonOverview.fxml
index 9a92c73e..ba359e77 100644
--- a/src/main/resources/view/PersonOverview.fxml
+++ b/src/main/resources/view/PersonOverview.fxml
@@ -6,24 +6,20 @@
-
+
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
diff --git a/src/main/resources/view/RootLayout.fxml b/src/main/resources/view/RootLayout.fxml
index ca03dece..57d54c99 100644
--- a/src/main/resources/view/RootLayout.fxml
+++ b/src/main/resources/view/RootLayout.fxml
@@ -8,14 +8,10 @@
-
+
@@ -32,6 +28,7 @@
@@ -39,6 +36,12 @@
-
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml
index b6eda81e..0389b9a2 100644
--- a/src/main/resources/view/StatusBarFooter.fxml
+++ b/src/main/resources/view/StatusBarFooter.fxml
@@ -9,11 +9,8 @@
-
-
-
-
-
+
+
diff --git a/src/test/java/address/TestApp.java b/src/test/java/address/TestApp.java
index b3323b6c..1abef2ee 100644
--- a/src/test/java/address/TestApp.java
+++ b/src/test/java/address/TestApp.java
@@ -1,27 +1,41 @@
package address;
import address.model.UserPrefs;
-import address.model.datatypes.AddressBook;
import address.model.datatypes.ReadOnlyAddressBook;
import address.storage.StorageAddressBook;
+import address.sync.RemoteManager;
+import address.sync.cloud.CloudManipulator;
+import address.sync.cloud.model.CloudAddressBook;
import address.util.Config;
import address.util.TestUtil;
+import javafx.stage.Stage;
import java.util.function.Supplier;
+/**
+ * This class is meant to override some properties of MainApp so that it will be suited for
+ * testing
+ */
public class TestApp extends MainApp {
public static final String SAVE_LOCATION_FOR_TESTING = TestUtil.appendToSandboxPath("sampleData.xml");
- protected Supplier initialDataSupplier = TestUtil::generateSampleAddressBook;
+ protected static final String DEFAULT_CLOUD_LOCATION_FOR_TESTING = TestUtil.appendToSandboxPath("sampleCloudData.xml");
+ protected Supplier initialDataSupplier = () -> null;
+ protected Supplier initialCloudDataSupplier = () -> null;
protected String saveFileLocation = SAVE_LOCATION_FOR_TESTING;
+ protected CloudManipulator remote;
+ public TestApp() {
+ }
- public TestApp(Supplier initialDataSupplier, String saveFileLocation) {
+ public TestApp(Supplier initialDataSupplier, String saveFileLocation,
+ Supplier initialCloudDataSupplier) {
super();
this.initialDataSupplier = initialDataSupplier;
this.saveFileLocation = saveFileLocation;
+ this.initialCloudDataSupplier = initialCloudDataSupplier;
- //If some intial data has been provided, write those to the file
+ // If some initial local data has been provided, write those to the file
if (initialDataSupplier.get() != null) {
TestUtil.createDataFileWithData(
new StorageAddressBook(this.initialDataSupplier.get()),
@@ -30,19 +44,44 @@ public TestApp(Supplier initialDataSupplier, String saveFil
}
@Override
- protected Config initConfig() {
- Config config = super.initConfig();
- config.appTitle = "Test App";
+ protected Config initConfig(String configFilePath) {
+ Config config = super.initConfig(configFilePath);
+ config.setAppTitle("Test App");
+ config.setLocalDataFilePath(saveFileLocation);
+ // Use default cloud test data if no data is supplied
+ if (initialCloudDataSupplier.get() == null) config.setCloudDataFilePath(DEFAULT_CLOUD_LOCATION_FOR_TESTING);
return config;
}
@Override
protected UserPrefs initPrefs(Config config) {
UserPrefs userPrefs = super.initPrefs(config);
- userPrefs.setSaveLocation(saveFileLocation);
return userPrefs;
}
+ @Override
+ protected RemoteManager initRemoteManager(Config config) {
+ if (initialCloudDataSupplier.get() == null) {
+ remote = new CloudManipulator(config);
+ } else {
+ remote = new CloudManipulator(config, initialCloudDataSupplier.get());
+ }
+ return new RemoteManager(remote);
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ ui.start(primaryStage);
+ updateManager.start();
+ storageManager.start();
+ syncManager.start();
+ remote.start(primaryStage);
+ }
+
+ public void deregisterHotKeys(){
+ keyBindingsManager.stop();
+ }
+
public static void main(String[] args) {
launch(args);
}
diff --git a/src/test/java/address/unittests/browser/BrowserManagerTest.java b/src/test/java/address/browser/BrowserManagerTest.java
similarity index 71%
rename from src/test/java/address/unittests/browser/BrowserManagerTest.java
rename to src/test/java/address/browser/BrowserManagerTest.java
index f75cc8c9..3c05f077 100644
--- a/src/test/java/address/unittests/browser/BrowserManagerTest.java
+++ b/src/test/java/address/browser/BrowserManagerTest.java
@@ -1,31 +1,34 @@
-package address.unittests.browser;
+package address.browser;
-import address.browser.BrowserManager;
-import address.util.JavafxThreadingRule;
+import address.util.TestUtil;
import hubturbo.embeddedbrowser.BrowserType;
import javafx.collections.FXCollections;
-import org.junit.Rule;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.concurrent.TimeoutException;
-import static junit.framework.TestCase.assertFalse;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+
/**
* Tests the BrowserManager behaviours and functionality.
*/
public class BrowserManagerTest {
- @Rule
- /**
- * To run test cases on JavaFX thread.
- */
- public JavafxThreadingRule javafxRule = new JavafxThreadingRule();
+ @BeforeClass
+ public static void setup() throws TimeoutException {
+ TestUtil.initRuntime();
+ TestUtil.initBrowserInStatic();
+ }
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
@Test
public void testNecessaryBrowserResources_resourcesNotNull() throws NoSuchMethodException,
@@ -39,6 +42,4 @@ public void testNecessaryBrowserResources_resourcesNotNull() throws NoSuchMethod
method.setAccessible(true);
method.invoke(manager);
}
-
-
}
diff --git a/src/test/java/address/browser/EmbeddedBrowserFactoryTest.java b/src/test/java/address/browser/EmbeddedBrowserFactoryTest.java
new file mode 100644
index 00000000..cdb0ea20
--- /dev/null
+++ b/src/test/java/address/browser/EmbeddedBrowserFactoryTest.java
@@ -0,0 +1,67 @@
+package address.browser;
+
+import address.util.PlatformExecUtil;
+import address.util.TestUtil;
+import hubturbo.EmbeddedBrowser;
+import hubturbo.embeddedbrowser.BrowserType;
+import hubturbo.embeddedbrowser.EmbeddedBrowserFactory;
+import hubturbo.embeddedbrowser.fxbrowser.FxBrowserAdapter;
+import hubturbo.embeddedbrowser.jxbrowser.JxBrowserAdapter;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * To test the EmbeddedBrowserFactory
+ */
+public class EmbeddedBrowserFactoryTest {
+
+ @BeforeClass
+ public static void setup() throws TimeoutException {
+ TestUtil.initRuntime();
+ TestUtil.initBrowserInStatic();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
+ @Test
+ public void testCreateBrowser_fullFeatureBrowser_success() {
+ EmbeddedBrowser browser = EmbeddedBrowserFactory.createBrowser(BrowserType.FULL_FEATURE_BROWSER);
+ assertNotNull(browser);
+ assertTrue(browser instanceof JxBrowserAdapter);
+
+ browser.dispose();
+ }
+
+ @Test
+ public void testCreateBrowser_limitedFeatureBrowser_success() {
+ final AtomicReference browser = new AtomicReference<>();
+ PlatformExecUtil.runLaterAndWait(() ->
+ browser.set(EmbeddedBrowserFactory.createBrowser(BrowserType.LIMITED_FEATURE_BROWSER)));
+ assertNotNull(browser.get());
+ assertTrue(browser.get() instanceof FxBrowserAdapter);
+
+ browser.get().dispose();
+ }
+
+ @Test
+ public void testCreateBrowser_invalidChoice_fail() {
+ EmbeddedBrowser browser = null;
+ try {
+ browser = EmbeddedBrowserFactory.createBrowser(BrowserType.valueOf("SUPER_FEATURE_BROWSER"));
+ } catch (IllegalArgumentException e) {
+ }
+ assertNull(browser);
+ }
+
+}
diff --git a/src/test/java/address/unittests/browser/EmbeddedBrowserObjectMapperTest.java b/src/test/java/address/browser/EmbeddedBrowserObjectMapperTest.java
similarity index 96%
rename from src/test/java/address/unittests/browser/EmbeddedBrowserObjectMapperTest.java
rename to src/test/java/address/browser/EmbeddedBrowserObjectMapperTest.java
index 7834b87f..7f8dd623 100644
--- a/src/test/java/address/unittests/browser/EmbeddedBrowserObjectMapperTest.java
+++ b/src/test/java/address/browser/EmbeddedBrowserObjectMapperTest.java
@@ -1,4 +1,4 @@
-package address.unittests.browser;
+package address.browser;
import hubturbo.embeddedbrowser.EbEditorCommand;
import hubturbo.embeddedbrowser.jxbrowser.EmbeddedBrowserObjectMapper;
diff --git a/src/test/java/address/unittests/browser/HyperBrowserTest.java b/src/test/java/address/browser/HyperBrowserTest.java
similarity index 83%
rename from src/test/java/address/unittests/browser/HyperBrowserTest.java
rename to src/test/java/address/browser/HyperBrowserTest.java
index fc935492..31e7f194 100644
--- a/src/test/java/address/unittests/browser/HyperBrowserTest.java
+++ b/src/test/java/address/browser/HyperBrowserTest.java
@@ -1,12 +1,14 @@
-package address.unittests.browser;
+package address.browser;
+import address.util.PlatformExecUtil;
+import address.util.TestUtil;
import hubturbo.embeddedbrowser.BrowserType;
import hubturbo.embeddedbrowser.HyperBrowser;
import hubturbo.embeddedbrowser.page.Page;
-import address.util.JavafxThreadingRule;
import address.util.UrlUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Test;
-import org.junit.Rule;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
@@ -15,6 +17,8 @@
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertTrue;
@@ -25,12 +29,6 @@
*/
public class HyperBrowserTest {
- @Rule
- /**
- * To run test cases on JavaFX thread.
- */
- public JavafxThreadingRule javafxRule = new JavafxThreadingRule();
-
List listOfUrl = Arrays.asList(new URL("https://github.com"),
new URL("https://google.com.sg"),
new URL("https://sg.yahoo.com"),
@@ -41,6 +39,17 @@ public class HyperBrowserTest {
public HyperBrowserTest() throws MalformedURLException {
}
+ @BeforeClass
+ public static void setup() throws TimeoutException {
+ TestUtil.initRuntime();
+ TestUtil.initBrowserInStatic();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
@Test
public void testFullFeatureBrowser_loadUrl_urlAssigned() throws MalformedURLException, InterruptedException {
HyperBrowser browser = new HyperBrowser(BrowserType.FULL_FEATURE_BROWSER, 1, Optional.empty());
@@ -132,26 +141,31 @@ public void testFullFeatureBrowser_loadUrlsThenLoadUrl_displayedUrlRemoved() thr
@Test
public void testLimitedFeatureBrowser_loadUrl_urlAssigned() throws MalformedURLException, InterruptedException {
- HyperBrowser browser = new HyperBrowser(BrowserType.LIMITED_FEATURE_BROWSER, 1, Optional.empty());
+ final AtomicReference browser = new AtomicReference<>();
+ PlatformExecUtil.runLaterAndWait(() -> browser.set(new HyperBrowser(BrowserType.LIMITED_FEATURE_BROWSER, 1, Optional.empty())));
URL url = new URL("https://github.com");
- Page page = browser.loadUrl(url).get(0);
- assertTrue(UrlUtil.compareBaseUrls(page.getBrowser().getOriginUrl(), url));
+ final AtomicReference page = new AtomicReference<>();
+ PlatformExecUtil.runLaterAndWait(() -> page.set(browser.get().loadUrl(url).get(0)));
+ assertTrue(UrlUtil.compareBaseUrls(page.get().getBrowser().getOriginUrl(), url));
+ browser.get().dispose();
}
@Test
public void testLimitedFeatureBrowser_loadUrls_urlsAssigned() throws MalformedURLException, IllegalAccessException, NoSuchFieldException, InterruptedException {
- HyperBrowser browser = new HyperBrowser(BrowserType.LIMITED_FEATURE_BROWSER, 3, Optional.empty());
- Page page = browser.loadUrls(listOfUrl.get(0), listOfUrl.subList(1,3)).get(0);
- assertTrue(UrlUtil.compareBaseUrls(page.getBrowser().getOriginUrl(), listOfUrl.get(0)));
-
- Field pages = browser.getClass().getDeclaredField("pages");
+ final AtomicReference browser = new AtomicReference<>();
+ PlatformExecUtil.runLaterAndWait(() -> browser.set(new HyperBrowser(BrowserType.LIMITED_FEATURE_BROWSER, 3, Optional.empty())));
+ final AtomicReference page = new AtomicReference<>();
+ PlatformExecUtil.runLaterAndWait(() -> page.set(browser.get().loadUrls(listOfUrl.get(0), listOfUrl.subList(1,3)).get(0)));
+ assertTrue(UrlUtil.compareBaseUrls(page.get().getBrowser().getOriginUrl(), listOfUrl.get(0)));
+ Field pages = browser.get().getClass().getDeclaredField("pages");
pages.setAccessible(true);
- List listOfPages = (List) pages.get(browser);
- listOfPages.remove(page);
+ List listOfPages = (List) pages.get(browser.get());
+ listOfPages.remove(page.get());
Page secondPage = listOfPages.remove(0);
assertTrue(UrlUtil.compareBaseUrls(secondPage.getBrowser().getOriginUrl(), listOfUrl.get(1)));
Page thirdPage = listOfPages.remove(0);
assertTrue(UrlUtil.compareBaseUrls(thirdPage.getBrowser().getOriginUrl(), listOfUrl.get(2)));
+ browser.get().dispose();
}
@Test
diff --git a/src/test/java/address/browser/PageTest.java b/src/test/java/address/browser/PageTest.java
new file mode 100644
index 00000000..a5abb751
--- /dev/null
+++ b/src/test/java/address/browser/PageTest.java
@@ -0,0 +1,93 @@
+package address.browser;
+
+import address.util.TestUtil;
+import hubturbo.EmbeddedBrowser;
+import hubturbo.embeddedbrowser.BrowserType;
+import hubturbo.embeddedbrowser.EbLoadListener;
+import hubturbo.embeddedbrowser.EmbeddedBrowserFactory;
+import hubturbo.embeddedbrowser.page.Page;
+import org.apache.commons.io.IOUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * To test the Page methods.
+ */
+public class PageTest {
+
+ private static final String VALID_ID_1 = "js-pjax-container";
+ private static final String VALID_CLASS_NAME_1 = "octicon octicon-repo";
+ public static final String VALID_CLASS_NAME_2 = "columns profilecols";
+ public static final String VALID_ID_2 = "js-flash-container";
+ public static final String VALID_ID_3 = "contributions-calendar";
+
+ @BeforeClass
+ public static void setup() throws TimeoutException {
+ TestUtil.initRuntime();
+ TestUtil.initBrowserInStatic();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
+ @Test
+ public void testPageTestMethods_fullFeatureBrowser() throws IOException, InterruptedException {
+ Page page = getSampleGithubProfilePage(BrowserType.FULL_FEATURE_BROWSER);
+
+ assertNotNull(page.getElementByClass(VALID_CLASS_NAME_1));
+ assertNull(page.getElementByClass("singaporeFlyer"));
+
+ assertNotNull(page.getElementById(VALID_ID_1));
+ assertNull(page.getElementById("maryland"));
+
+ assertTrue(page.verifyPresence(new String[]{VALID_ID_1, VALID_CLASS_NAME_1}));
+ assertFalse(page.verifyPresence(new String[]{VALID_ID_1, "lalaland"}));
+
+ assertTrue(page.verifyPresenceByClassNames(new String[]{VALID_CLASS_NAME_1, VALID_CLASS_NAME_2}));
+ assertFalse(page.verifyPresenceByClassNames(new String[]{"disney", VALID_CLASS_NAME_2}));
+
+ assertTrue(page.verifyPresenceByClassNames(VALID_CLASS_NAME_2));
+ assertFalse(page.verifyPresenceByClassNames("disney"));
+
+ assertTrue(page.verifyPresenceByIds(new String[]{VALID_ID_1, VALID_ID_2, VALID_ID_3}));
+ assertFalse(page.verifyPresenceByIds(new String[]{"hubturbo", VALID_ID_2, "teammates"}));
+
+ assertTrue(page.verifyPresenceByIds(VALID_ID_1));
+ assertFalse(page.verifyPresenceByIds("hubturbo"));
+
+ page.getBrowser().dispose();
+
+ }
+
+ private Page getSampleGithubProfilePage(BrowserType type) throws IOException, InterruptedException {
+ EmbeddedBrowser browser = EmbeddedBrowserFactory.createBrowser(type);
+ InputStream stream = this.getClass().getResourceAsStream("/html_pages/github_profile_page.html");
+ String html = IOUtils.toString(stream);
+ stream.close();
+ Page page = new Page(browser);
+ CountDownLatch latch = new CountDownLatch(1);
+ page.setPageLoadFinishListener(b -> {
+ if (b) {
+ latch.countDown();
+ }
+ });
+ browser.loadHTML(html);
+ latch.await(5, TimeUnit.SECONDS);
+ return new Page(browser);
+ }
+
+}
diff --git a/src/test/java/address/guitests/FullSystemTest.java b/src/test/java/address/guitests/FullSystemTest.java
deleted file mode 100644
index 1a8d74e2..00000000
--- a/src/test/java/address/guitests/FullSystemTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package address.guitests;
-
-import javafx.scene.input.KeyCode;
-import org.junit.Test;
-
-import static org.testfx.api.FxAssert.verifyThat;
-import static org.testfx.matcher.base.NodeMatchers.hasText;
-
-public class FullSystemTest extends GuiTestBase {
- @Test
- public void scenarioOne() {
- // Attempt to create new tag, then cancel
- clickOn("Tags").clickOn("New Tag");
- verifyThat("#tagNameField", hasText(""));
- push(KeyCode.ESCAPE);
-
- // Attempt to create new tag
- clickOn("Tags").clickOn("New Tag");
- verifyThat("#tagNameField", hasText(""));
-
- // Set name of tag to be "colleagues"
- clickOn("#tagNameField").write("colleagues");
- verifyThat("#tagNameField", hasText("colleagues"));
- type(KeyCode.ENTER);
-
- // Edit Hans Muster to John Tan, and edit details
- clickOn("Muster").type(KeyCode.E)
- .clickOn("#firstNameField").push(KeyCode.SHORTCUT, KeyCode.A).eraseText(1).write("John")
- .clickOn("#lastNameField").eraseText(6).write("Tan")
- .clickOn("#cityField").write("Singapore")
- .clickOn("#githubUserNameField").write("john123")
- .clickOn("#tagList")
- .sleep(200) // wait for opening animation
- .clickOn("#tagSearch")
- .write("coll").type(KeyCode.SPACE)
- .type(KeyCode.ENTER)
- .sleep(200); // wait for closing animation
- verifyThat("#firstNameField", hasText("John"));
- verifyThat("#lastNameField", hasText("Tan"));
- verifyThat("#cityField", hasText("Singapore"));
- verifyThat("#githubUserNameField", hasText("john123"));
- type(KeyCode.ENTER);
-
- // filter persons list with "colleagues" tag
- clickOn("#filterField").write("tag:colleagues").type(KeyCode.ENTER);
- verifyThat("#filterField", hasText("tag:colleagues"));
-
- // verify John is in the list, and try to delete
- clickOn("John").type(KeyCode.D);
-
- // remove filter again
- clickOn("#filterField").push(KeyCode.SHORTCUT, KeyCode.A).eraseText(1).type(KeyCode.ENTER);
-
- // edit Ruth Mueller's github username
- clickOn("Ruth").type(KeyCode.E).clickOn("#streetField").write("My Street")
- .clickOn("#githubUserNameField").write("ruth321")
- .type(KeyCode.ENTER);
-
- // filter based on "Mueller" name
- clickOn("#filterField").write("name:Mueller").type(KeyCode.ENTER);
-
- // edit Martin Mueller's tags
- clickOn("Martin").type(KeyCode.E)
- .clickOn("#tagList")
- .sleep(200)// wait for opening animation
- .write("frien").type(KeyCode.SPACE)
- .type(KeyCode.ENTER)
- .sleep(200)// wait for closing animation
- .type(KeyCode.ENTER);
-
- // ensure "About" dialog opens
- clickOn("Help").clickOn("About").clickOn("OK");
-
- // create a new person Ming Lee, check that last name cannot be blank
- clickOn("New")
- .clickOn("#firstNameField").write("Ming").clickOn("OK")
- .targetWindow("Invalid Fields").clickOn("OK")
- .clickOn("#lastNameField").write("Lee").clickOn("OK");
-
- // save file
- clickOn("File").clickOn("[Local] Save");
-
- // add new tag "company"
- clickOn("Tags").clickOn("Manage Tags")
- .rightClickOn("colleagues").clickOn("New")
- .clickOn("#tagNameField").write("company");
- verifyThat("#tagNameField", hasText("company"));
- push(KeyCode.ENTER);
-
- // verify that company is in the tag list
- rightClickOn("company").clickOn("Edit");
- verifyThat("#tagNameField", hasText("company"));
-
- // UNABLE to launch file chooser in mac's headless mode
- // UNABLE to close tag list dialog in headless mode
- }
-}
diff --git a/src/test/java/address/guitests/GuiTestBase.java b/src/test/java/address/guitests/GuiTestBase.java
deleted file mode 100644
index 5f6dc681..00000000
--- a/src/test/java/address/guitests/GuiTestBase.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package address.guitests;
-
-import address.TestApp;
-import address.events.EventManager;
-import address.keybindings.KeyBinding;
-import address.keybindings.KeySequence;
-import address.model.datatypes.ReadOnlyAddressBook;
-import address.util.TestUtil;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyCodeCombination;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.testfx.api.FxRobot;
-import org.testfx.api.FxToolkit;
-
-import java.util.concurrent.TimeoutException;
-
-public class GuiTestBase extends FxRobot {
-
- @BeforeClass
- public static void setupSpec() {
- try {
- FxToolkit.registerPrimaryStage();
- FxToolkit.hideStage();
- } catch (TimeoutException e) {
- e.printStackTrace();
- }
- }
-
- @Before
- public void setup() throws Exception {
- EventManager.clearSubscribers();
- FxToolkit.setupApplication(() -> new TestApp(this::getInitialData, getDataFileLocation()));
- FxToolkit.showStage();
- }
-
- /**
- * Override this in child classes to set the initial data.
- * Return null to use the data in the file specified in {@link #getDataFileLocation()}
- */
- protected ReadOnlyAddressBook getInitialData() {
- return TestUtil.generateSampleAddressBook();
- }
-
- /**
- * Override this in child classes to set the data file location.
- * @return
- */
- protected String getDataFileLocation(){
- return TestApp.SAVE_LOCATION_FOR_TESTING;
- }
-
- @After
- public void cleanup() throws TimeoutException {
- FxToolkit.cleanupStages();
- }
-
- public FxRobot push(KeyCode... keyCodes){
- return super.push(TestUtil.scrub(keyCodes));
- }
-
-
- public FxRobot push(KeyCodeCombination keyCodeCombination){
- return super.push(TestUtil.scrub(keyCodeCombination));
- }
-
- protected FxRobot push(KeyBinding keyBinding){
- KeyCodeCombination keyCodeCombination = (KeyCodeCombination)keyBinding.getKeyCombination();
- return this.push(TestUtil.scrub(keyCodeCombination));
- }
-
- public FxRobot press(KeyCode... keyCodes) {
- return super.press(TestUtil.scrub(keyCodes));
- }
-
- public FxRobot release(KeyCode... keyCodes) {
- return super.release(TestUtil.scrub(keyCodes));
- }
-
- public FxRobot type(KeyCode... keyCodes) {
- return super.type(TestUtil.scrub(keyCodes));
- }
-
- protected void delay(int milliseconds) {
- try {
- Thread.sleep(milliseconds);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- protected void pushKeySequence(KeySequence keySequence) {
- push((KeyCodeCombination)keySequence.getKeyCombination());
- push((KeyCodeCombination)keySequence.getSecondKeyCombination());
- }
-
-}
diff --git a/src/test/java/address/guitests/KeyBindingsGuiTest.java b/src/test/java/address/guitests/KeyBindingsGuiTest.java
deleted file mode 100644
index 2c5ce575..00000000
--- a/src/test/java/address/guitests/KeyBindingsGuiTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package address.guitests;
-
-import address.keybindings.Bindings;
-import address.model.datatypes.AddressBook;
-import address.model.datatypes.ReadOnlyAddressBook;
-import address.model.datatypes.person.Person;
-import javafx.scene.input.KeyCode;
-import org.junit.Test;
-
-import static org.testfx.api.FxAssert.verifyThat;
-import static org.testfx.matcher.base.NodeMatchers.hasText;
-
-/**
- * Tests key bindings through the GUI
- */
-public class KeyBindingsGuiTest extends GuiTestBase {
-
- private final Bindings bindings = new Bindings();
-
- @Override
- protected ReadOnlyAddressBook getInitialData() {
- AddressBook ab = new AddressBook();
- ab.addPerson(new Person("Person1", "Lastname1", 1));
- ab.addPerson(new Person("Person2", "Lastname2", 2));
- ab.addPerson(new Person("Person3", "Lastname3", 3));
- ab.addPerson(new Person("Person4", "Lastname4", 4));
- ab.addPerson(new Person("Person5", "Lastname5", 5));
- return ab;
- //TODO: create a better set of sample data
- }
-
- @Test
- public void keyBindings(){
-
- //======= shortcuts =======================
-
- push(bindings.LIST_ENTER_SHORTCUT);
- verifyPersonSelected("Person1", "Lastname1");
-
- push(KeyCode.CONTROL, KeyCode.DIGIT3);
- verifyPersonSelected("Person3", "Lastname3");
-
- //======= sequences =========================
-
- pushKeySequence(bindings.LIST_GOTO_BOTTOM_SEQUENCE);
- verifyPersonSelected("Person5", "Lastname5");
-
- pushKeySequence(bindings.LIST_GOTO_TOP_SEQUENCE);
- verifyPersonSelected("Person1", "Lastname1");
-
-
- //======= accelerators =======================
-
- push(KeyCode.CONTROL, KeyCode.DIGIT3);
- push(bindings.PERSON_EDIT_ACCELERATOR);
- verifyEditWindowOpened("Person3", "Lastname3");
-
- push(bindings.PERSON_DELETE_ACCELERATOR);
- verifyPersonDeleted("Person3", "Lastname3");
-
- //TODO: test tag, file open, new, save, save as, cancel
-
- //======== others ============================
-
- pushKeySequence(bindings.LIST_GOTO_BOTTOM_SEQUENCE);
- push(KeyCode.UP);
- verifyPersonSelected("Person4", "Lastname4");
-
- push(KeyCode.DOWN);
- verifyPersonSelected("Person5", "Lastname5");
-
- //======== hotkeys ============================
-
- //TODO: test hotkeys
-
- }
-
-
- private void verifyPersonDeleted(String firstName, String lastName) {
- //TODO: implement this
- }
-
- private void verifyEditWindowOpened(String firstName, String lastName) {
- targetWindow("Edit Person");
- verifyThat("#firstNameField", hasText(firstName));
- verifyThat("#lastNameField", hasText(lastName));
- clickOn("Cancel");
- }
-
- private void verifyPersonSelected(String firstName, String lastName) {
- push(bindings.PERSON_EDIT_ACCELERATOR);
- verifyEditWindowOpened(firstName, lastName);
- }
-
-}
diff --git a/src/test/java/address/guitests/PersonOverviewTest.java b/src/test/java/address/guitests/PersonOverviewTest.java
deleted file mode 100644
index 64ca5dd3..00000000
--- a/src/test/java/address/guitests/PersonOverviewTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package address.guitests;
-
-import javafx.scene.control.Label;
-import org.junit.Test;
-
-import static org.junit.Assert.assertTrue;
-
-public class PersonOverviewTest extends GuiTestBase {
- @Test
- public void dragAndDrop_firstToSecond() {
- Label hansNameLabel = getNameLabelOf("Hans");
- Label ruthIdLabel = getNameLabelOf("Ruth");
- assertTrue(hansNameLabel.localToScreen(0, 0).getY() < ruthIdLabel.localToScreen(0, 0).getY());
- drag("Hans").dropTo("Heinz");// drag from first to start of 3rd (slightly further down between 2nd and 3rd)
-
- Label hansNameLabel2 = getNameLabelOf("Hans");
- Label ruthIdLabel2 = getNameLabelOf("Ruth");
- assertTrue(hansNameLabel2.localToScreen(0, 0).getY() > ruthIdLabel2.localToScreen(0, 0).getY());
- }
-
- private Label getNameLabelOf(String name) {
- return (Label) lookup(name).tryQuery().get();
- }
-}
diff --git a/src/test/java/address/keybindings/AcceleratorTest.java b/src/test/java/address/keybindings/AcceleratorTest.java
new file mode 100644
index 00000000..b4919389
--- /dev/null
+++ b/src/test/java/address/keybindings/AcceleratorTest.java
@@ -0,0 +1,16 @@
+package address.keybindings;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class AcceleratorTest {
+
+ Accelerator accelerator = new Accelerator("Dummy accelerator", KeyBindingTest.SHIFT_B);
+
+ @Test
+ public void toStringMethod() throws Exception {
+ assertEquals("Accelerator Dummy accelerator Shift+B", accelerator.toString());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/address/keybindings/BindingsTest.java b/src/test/java/address/keybindings/BindingsTest.java
new file mode 100644
index 00000000..7883b2fb
--- /dev/null
+++ b/src/test/java/address/keybindings/BindingsTest.java
@@ -0,0 +1,70 @@
+package address.keybindings;
+
+import address.events.KeyBindingEvent;
+import javafx.scene.input.KeyCodeCombination;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class BindingsTest {
+
+ private Bindings bindings = new Bindings();
+
+ @Test
+ public void findMatchingSequence() throws Exception {
+ KeyBindingEvent keyEventG = getKeyEvent("G");
+ KeyBindingEvent keyEventB = getKeyEvent("B");
+ assertTrue(KeySequence.isElapsedTimePermissibile(keyEventG.time, keyEventB.time));
+ assertEquals(bindings.LIST_GOTO_BOTTOM_SEQUENCE, bindings.findMatchingSequence(keyEventG, keyEventB).get());
+ }
+
+ private KeyBindingEvent getKeyEvent(String keyCombo) {
+ return new KeyBindingEvent(KeyCodeCombination.valueOf(keyCombo));
+ }
+
+ @Test
+ public void getHotkeys() throws Exception {
+ assertEquals(bindings.hotkeys, bindings.getHotkeys());
+ }
+
+ @Test
+ public void getAccelerators() throws Exception {
+ assertEquals(bindings.accelerators, bindings.getAccelerators());
+ }
+
+ @Test
+ public void getSequences() throws Exception {
+ assertEquals(bindings.sequences, bindings.getSequences());
+ }
+
+ @Test
+ public void getBinding() throws Exception {
+ //verifying a sequence
+ KeyBindingEvent keyEventG = getKeyEvent("G");
+ KeyBindingEvent keyEventB = getKeyEvent("B");
+ assertTrue(KeySequence.isElapsedTimePermissibile(keyEventG.time, keyEventB.time));
+ assertEquals(bindings.LIST_GOTO_BOTTOM_SEQUENCE, bindings.getBinding(keyEventG, keyEventB).get());
+
+ //verifyng a shortcut
+ KeyBindingEvent keyEventShortcutDown = getKeyEvent("SHORTCUT + DOWN");
+ assertEquals(bindings.LIST_ENTER_SHORTCUT, bindings.getBinding(keyEventB, keyEventShortcutDown).get());
+
+ //verifying an accelerator
+ KeyBindingEvent keyEventD = getKeyEvent("D");
+ assertEquals(bindings.PERSON_DELETE_ACCELERATOR, bindings.getBinding(keyEventShortcutDown, keyEventD).get());
+
+ //verifying an hotkey
+ KeyBindingEvent keyEventMetaAltX = getKeyEvent("META + ALT + X");
+ assertEquals(bindings.APP_MINIMIZE_HOTKEY.get(1), bindings.getBinding(keyEventD, keyEventMetaAltX).get());
+ }
+
+ @Test
+ public void getAllBindings() throws Exception {
+ int totalBindings = bindings.accelerators.size()
+ + bindings.hotkeys.size()
+ + bindings.sequences.size()
+ + bindings.shortcuts.size();
+ assertEquals(totalBindings, bindings.getAllBindings().size());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/address/keybindings/GlobalHotkeyProviderTest.java b/src/test/java/address/keybindings/GlobalHotkeyProviderTest.java
new file mode 100644
index 00000000..7b421b67
--- /dev/null
+++ b/src/test/java/address/keybindings/GlobalHotkeyProviderTest.java
@@ -0,0 +1,57 @@
+package address.keybindings;
+
+import address.events.BaseEvent;
+import address.events.EventManager;
+import address.events.GlobalHotkeyEvent;
+import address.util.LoggerManager;
+import com.tulskiy.keymaster.common.Provider;
+import javafx.scene.input.KeyCodeCombination;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import javax.swing.*;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class GlobalHotkeyProviderTest {
+
+ EventManager eventManagerMock = Mockito.mock(EventManager.class);
+ GlobalHotkeyProvider globalHotkeyProvider;
+ Provider providerMock = Mockito.mock(Provider.class);
+
+ @Before
+ public void setup(){
+ globalHotkeyProvider = new GlobalHotkeyProvider(eventManagerMock, LoggerManager.getLogger(KeyBindingsManager.class));
+ globalHotkeyProvider.provider = providerMock;
+ }
+
+ @Test
+ public void registerGlobalHotkeys() throws Exception {
+ List hotkeys = new Bindings().getHotkeys();
+ globalHotkeyProvider.registerGlobalHotkeys(hotkeys);
+
+ // Verify that the number of hotkeys registered is same as the number of keys in the list
+ verify(providerMock, times(hotkeys.size())).register(any(KeyStroke.class), Matchers.any());
+ }
+
+ @Test
+ public void handleGlobalHotkeyEvent() throws Exception {
+ globalHotkeyProvider.handleGlobalHotkeyEvent(new GlobalHotkeyEvent(KeyCodeCombination.valueOf("SHIFT + A")));
+
+ //verify an event was posted
+ verify(eventManagerMock, times(1)).post(any(BaseEvent.class));
+ }
+
+ @Test
+ public void clear() throws Exception {
+ globalHotkeyProvider.clear();
+ verify(providerMock, times(1)).reset();
+ verify(providerMock, times(1)).stop();
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/address/keybindings/GlobalHotkeyTest.java b/src/test/java/address/keybindings/GlobalHotkeyTest.java
new file mode 100644
index 00000000..c05fe8a4
--- /dev/null
+++ b/src/test/java/address/keybindings/GlobalHotkeyTest.java
@@ -0,0 +1,24 @@
+package address.keybindings;
+
+import javafx.scene.input.KeyCodeCombination;
+import org.junit.Test;
+
+import javax.swing.*;
+
+import static org.junit.Assert.*;
+
+public class GlobalHotkeyTest {
+ GlobalHotkey globalHotkey = new GlobalHotkey("Sample hotkey",
+ KeyCodeCombination.valueOf("META + ALT + X"),
+ KeyBindingTest.SAMPLE_EVENT);
+ @Test
+ public void getKeyStroke() throws Exception {
+ assertEquals(KeyStroke.getKeyStroke("meta alt X"), globalHotkey.getKeyStroke());
+ }
+
+ @Test
+ public void toStringMethod() throws Exception {
+ assertEquals("Global Hotkey Sample hotkey Alt+Meta+X", globalHotkey.toString());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/address/keybindings/KeyBindingTest.java b/src/test/java/address/keybindings/KeyBindingTest.java
new file mode 100644
index 00000000..2ac79dfc
--- /dev/null
+++ b/src/test/java/address/keybindings/KeyBindingTest.java
@@ -0,0 +1,64 @@
+package address.keybindings;
+
+
+import address.events.AcceleratorIgnoredEvent;
+import address.events.BaseEvent;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+
+public class KeyBindingTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ static KeyCombination ALT_A = KeyCodeCombination.valueOf("ALT + A");
+ static KeyCombination SHIFT_B = KeyCodeCombination.valueOf("SHIFT + B");
+ static BaseEvent SAMPLE_EVENT = new AcceleratorIgnoredEvent("Dummy");
+
+ private final String SAMPLE_KEYBINDING_NAME = "dummy name";
+ private KeyBinding keyBinding = new Shortcut(SAMPLE_KEYBINDING_NAME, ALT_A, SAMPLE_EVENT);
+
+ @Test
+ public void constructor_nullParameters_assertionFailure(){
+ // Null name
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("name cannot be null");
+ new Shortcut(null, ALT_A, SAMPLE_EVENT);
+
+ // Null key combo
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("key combination cannot be null");
+ new Shortcut(SAMPLE_KEYBINDING_NAME, null, SAMPLE_EVENT);
+
+ // Null event
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("event cannot be null");
+ new Shortcut(SAMPLE_KEYBINDING_NAME, SHIFT_B, null);
+ }
+
+ @Test
+ public void getText(){
+ assertEquals(SAMPLE_KEYBINDING_NAME + " Alt+A", keyBinding.getDisplayText());
+ }
+
+ @Test
+ public void getKeyCombination() throws Exception {
+ assertEquals(ALT_A, keyBinding.getKeyCombination());
+ }
+
+ @Test
+ public void getName() throws Exception {
+ assertEquals(SAMPLE_KEYBINDING_NAME, keyBinding.getName());
+ }
+
+ @Test
+ public void getEventToRaise() throws Exception {
+ assertEquals(SAMPLE_EVENT, keyBinding.getEventToRaise());
+ }
+
+}
diff --git a/src/test/java/address/unittests/keybindings/KeyBindingsManagerApiTest.java b/src/test/java/address/keybindings/KeyBindingsManagerApiTest.java
similarity index 80%
rename from src/test/java/address/unittests/keybindings/KeyBindingsManagerApiTest.java
rename to src/test/java/address/keybindings/KeyBindingsManagerApiTest.java
index f7b35f7c..9afa927c 100644
--- a/src/test/java/address/unittests/keybindings/KeyBindingsManagerApiTest.java
+++ b/src/test/java/address/keybindings/KeyBindingsManagerApiTest.java
@@ -1,13 +1,10 @@
-package address.unittests.keybindings;
+package address.keybindings;
import address.events.*;
-import address.keybindings.Accelerator;
-import address.keybindings.Bindings;
-import address.keybindings.KeyBinding;
-import address.keybindings.KeySequence;
import address.util.TestUtil;
import javafx.scene.input.KeyCombination;
+import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -15,15 +12,13 @@
import org.mockito.Matchers;
import org.mockito.Mockito;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.IntStream;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.*;
public class KeyBindingsManagerApiTest {
@@ -33,11 +28,16 @@ public class KeyBindingsManagerApiTest {
@Before
public void setup(){
- eventManagerSpy = Mockito.spy(EventManager.getInstance());
+ eventManagerSpy = Mockito.mock(EventManager.class);
keyBindingsManager = new KeyBindingsManagerEx();
keyBindingsManager.setEventManager(eventManagerSpy);
}
+ @After
+ public void tearDown() throws Exception {
+ keyBindingsManager.stop();
+ }
+
@Test
public void keyEventDetected_definedBinding_matchingEventRaised() throws Exception {
@@ -52,17 +52,14 @@ public void keyEventDetected_definedBinding_matchingEventRaised() throws Excepti
verifyAccelerator("PERSON_EDIT_ACCELERATOR", "E");
verifySequence(new JumpToListRequestEvent(-1), "G", "B");
verifySequence(new JumpToListRequestEvent(1), "G", "T");
- verifyAccelerator("FILE_NEW_ACCELERATOR", "SHORTCUT + N");
- verifyAccelerator("FILE_OPEN_ACCELERATOR", "SHORTCUT + O");
- verifyAccelerator("FILE_SAVE_ACCELERATOR", "SHORTCUT + S");
- verifyAccelerator("FILE_SAVE_AS_ACCELERATOR", "SHORTCUT + ALT + S");
verifyHotkey(new MinimizeAppRequestEvent(), "CTRL + ALT + X");
verifyHotkey(new MinimizeAppRequestEvent(), "META + ALT + X");
- verifyHotkey(new MaximizeAppRequestEvent(), "CTRL + SHIFT + X");
- verifyHotkey(new MaximizeAppRequestEvent(), "META + SHIFT + X");
+ verifyHotkey(new ResizeAppRequestEvent(), "CTRL + SHIFT + X");
+ verifyHotkey(new ResizeAppRequestEvent(), "META + SHIFT + X");
verifyAccelerator("PERSON_CHANGE_CANCEL_ACCELERATOR", "SHORTCUT + Z");
+ verifyAccelerator("HELP_PAGE_ACCELERATOR", "F1");
/*====== other keys ======================================================*/
verifyShortcut(new JumpToListRequestEvent(1), "SHORTCUT DOWN");
@@ -107,11 +104,8 @@ private void verify(BaseEvent expectedEvent, String... keyCombination) {
//As the mocked object is reused multiple times within a single @Test, we reset the mock before each sub test
Mockito.reset(eventManagerSpy);
- //simulate the key events that matches the key binding
- simulateKeyEvents(keyCombination);
-
- //verify that the simulated key event was detected
- Mockito.verify(eventManagerSpy, times(keyCombination.length)).post(Matchers.isA(KeyBindingEvent.class));
+ //simulate receiving the key events that matches the key binding
+ simulateReceivingKeyBindingEvents(keyCombination);
//verify that the correct event was raised
Mockito.verify(eventManagerSpy, times(1)).post((BaseEvent) argThat(new EventIntentionMatcher(expectedEvent)));
@@ -125,24 +119,22 @@ private void verifyIgnored(String... keyCombination) {
//As the mocked object is reused multiple times within a single @Test, we reset the mock before each sub test
Mockito.reset(eventManagerSpy);
- //simulate the key events that matches the key binding
- simulateKeyEvents(keyCombination);
-
- //verify that the simulated key event was detected
- Mockito.verify(eventManagerSpy, times(keyCombination.length)).post(Matchers.isA(KeyBindingEvent.class));
+ //simulate receiving key events that matches the key binding
+ simulateReceivingKeyBindingEvents(keyCombination);
//verify that no other event was raised i.e. only the simulated key event was posted to event manager
- Mockito.verify(eventManagerSpy, times(keyCombination.length)).post(Matchers.any());
+ Mockito.verify(eventManagerSpy, times(0)).post(Matchers.any());
}
/**
- * Simulates one or two key combinations being used
+ * Simulates receiving 1 or 2 key binding events from the event handling mechanism
* @param keyCombination
*/
- private void simulateKeyEvents(String[] keyCombination) {
+ private void simulateReceivingKeyBindingEvents(String[] keyCombination) {
assert keyCombination.length == 1 || keyCombination.length == 2;
+
Arrays.stream(keyCombination)
- .forEach(kc -> eventManagerSpy.post(new KeyBindingEvent(TestUtil.getKeyEvent(kc))));
+ .forEach(kc -> keyBindingsManager.handleKeyBindingEvent(new KeyBindingEvent(TestUtil.getKeyEvent(kc))));
}
/**
@@ -156,14 +148,15 @@ private void markKeyBindingAsTested(String[] keyCombos) {
KeyBindingEvent previousEvent;
if(keyCombos.length == 2){
- currentEvent = new KeyBindingEvent(TestUtil.getKeyEvent(keyCombos[1]));
+ //Note: previousEvent should be created first so that it's time stamp is earlier than currentEvent's
previousEvent = new KeyBindingEvent(TestUtil.getKeyEvent(keyCombos[0]));
+ currentEvent = new KeyBindingEvent(TestUtil.getKeyEvent(keyCombos[1]));
}else {
- currentEvent = new KeyBindingEvent(TestUtil.getKeyEvent(keyCombos[0]));
previousEvent = null;
+ currentEvent = new KeyBindingEvent(TestUtil.getKeyEvent(keyCombos[0]));
}
- Optional extends KeyBinding> tested = keyBindingsManager.getBinding(currentEvent, previousEvent);
+ Optional extends KeyBinding> tested = keyBindingsManager.getBinding(previousEvent, currentEvent);
yetToTest.remove(tested.get());
}
@@ -205,6 +198,12 @@ private void verifyNotInSequences(List sequences, Accelerator a) {
assertFalse("Clash between " + matchingSequence + " and " + a, matchingSequence.isPresent());
}
+ @Test
+ public void stop(){
+ //Not tested here because it is hard to veryify the effect of this method on jkeymaster
+ // without being able to fire hotkeys
+ }
+
/**
* A custom argument matcher used to compare two events to see if they have the same intention.
*/
diff --git a/src/test/java/address/unittests/keybindings/KeyBindingsManagerEx.java b/src/test/java/address/keybindings/KeyBindingsManagerEx.java
similarity index 74%
rename from src/test/java/address/unittests/keybindings/KeyBindingsManagerEx.java
rename to src/test/java/address/keybindings/KeyBindingsManagerEx.java
index 4fdad41e..c18d6315 100644
--- a/src/test/java/address/unittests/keybindings/KeyBindingsManagerEx.java
+++ b/src/test/java/address/keybindings/KeyBindingsManagerEx.java
@@ -1,8 +1,6 @@
-package address.unittests.keybindings;
+package address.keybindings;
import address.events.KeyBindingEvent;
-import address.keybindings.KeyBinding;
-import address.keybindings.KeyBindingsManager;
import java.util.List;
import java.util.Optional;
@@ -18,8 +16,8 @@ public class KeyBindingsManagerEx extends KeyBindingsManager {
* @param currentEvent the most recent key event
* @param previousEvent the previous key event
*/
- public Optional extends KeyBinding> getBinding(KeyBindingEvent currentEvent, KeyBindingEvent previousEvent) {
- return BINDINGS.getBinding(currentEvent, previousEvent);
+ public Optional extends KeyBinding> getBinding(KeyBindingEvent previousEvent, KeyBindingEvent currentEvent) {
+ return BINDINGS.getBinding(previousEvent, currentEvent);
}
/** Returns {@link address.keybindings.KeyBinding} objects managed.
diff --git a/src/test/java/address/keybindings/KeyBindingsManagerTest.java b/src/test/java/address/keybindings/KeyBindingsManagerTest.java
new file mode 100644
index 00000000..da52b21e
--- /dev/null
+++ b/src/test/java/address/keybindings/KeyBindingsManagerTest.java
@@ -0,0 +1,84 @@
+package address.keybindings;
+
+import address.events.*;
+import javafx.scene.input.KeyCodeCombination;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+public class KeyBindingsManagerTest {
+ KeyBindingsManager keyBindingsManager;
+ EventManager eventManagerMock;
+
+ @Before
+ public void setUp() throws Exception {
+ keyBindingsManager = new KeyBindingsManager();
+ eventManagerMock = Mockito.mock(EventManager.class);
+ keyBindingsManager.setEventManager(eventManagerMock);
+ Bindings bindingsMock = Mockito.mock(Bindings.class);
+ keyBindingsManager.BINDINGS = bindingsMock;
+ keyBindingsManager.hotkeyProvider = Mockito.mock(GlobalHotkeyProvider.class);
+ }
+
+ @After
+ public void tearDown()throws Exception{
+ keyBindingsManager.stop();
+ }
+
+ @Test
+ public void handleKeyBindingEvent_nonExistentKeyBinding_eventNotRaised() throws Exception {
+ //Set up mock to return nothing
+ doReturn(Optional.empty()).when(keyBindingsManager.BINDINGS).getBinding(anyObject(), anyObject());
+
+ //Call SUT with non-existent keybinding event
+ keyBindingsManager.handleKeyBindingEvent(new KeyBindingEvent(KeyCodeCombination.valueOf("N")));
+
+ //Verify event is not called
+ verify(eventManagerMock, times(0)).post(any());
+ }
+
+ @Test
+ public void handleKeyBindingEvent_existingKeyBinding_eventRaised() throws Exception {
+ //Set up mock to return a known accelerator
+ KeyBinding deleteAccelerator = new Bindings().PERSON_DELETE_ACCELERATOR;
+ doReturn(Optional.of(deleteAccelerator)).when(keyBindingsManager.BINDINGS).getBinding(anyObject(), anyObject());
+
+ //Invoke SUT
+ keyBindingsManager.handleKeyBindingEvent(new KeyBindingEvent(deleteAccelerator.keyCombination));
+
+ //Verify the event was raised
+ verify(eventManagerMock, times(1)).post(Matchers.isA(AcceleratorIgnoredEvent.class));
+ }
+
+ @Test
+ public void stop() throws Exception {
+ keyBindingsManager.stop();
+ verify(keyBindingsManager.hotkeyProvider, times(1)).clear();
+ }
+
+ @Test
+ public void getAcceleratorKeyCombo() throws Exception {
+ // Set up the mock to return a list containing two accelerators
+ ArrayList accelerators = new ArrayList<>();
+ Accelerator personEditAccelerator = new Bindings().PERSON_EDIT_ACCELERATOR;
+ accelerators.add(personEditAccelerator);
+ accelerators.add(new Bindings().PERSON_CHANGE_CANCEL_ACCELERATOR);
+
+ // Verify checking for non-existent accelerator
+ assertEquals(Optional.empty(), keyBindingsManager.getAcceleratorKeyCombo("non existent"));
+
+ // Verify checking for existing accelerator
+ when(keyBindingsManager.BINDINGS.getAccelerators()).thenReturn(accelerators);
+ assertEquals(personEditAccelerator.getKeyCombination(),
+ keyBindingsManager.getAcceleratorKeyCombo("PERSON_EDIT_ACCELERATOR").get());
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/address/keybindings/KeySequenceTest.java b/src/test/java/address/keybindings/KeySequenceTest.java
new file mode 100644
index 00000000..4d23ee66
--- /dev/null
+++ b/src/test/java/address/keybindings/KeySequenceTest.java
@@ -0,0 +1,99 @@
+package address.keybindings;
+
+
+import javafx.scene.input.KeyCodeCombination;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class KeySequenceTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ @SuppressWarnings(value = "Used to test deprecated method")
+ public void constructor_invalidConstructor_assertionFailure() throws Exception {
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("Invalid constructor called");
+ new KeySequence("dummy name", KeyBindingTest.ALT_A, KeyBindingTest.SAMPLE_EVENT);
+ }
+
+ @Test
+ public void constructor_nullSecondKeyCombo_assertionFailure() throws Exception {
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("Second key combination cannot be null");
+ new KeySequence("dummy name", KeyBindingTest.ALT_A, null, KeyBindingTest.SAMPLE_EVENT);
+ }
+
+ @Test
+ public void getSecondKeyCombination() throws Exception {
+ KeySequence keySequence = new KeySequence("Sample Key Sequence", KeyBindingTest.ALT_A, KeyBindingTest.SHIFT_B, KeyBindingTest.SAMPLE_EVENT);
+ assertEquals("Shift+B", keySequence.getSecondKeyCombination().getDisplayText());
+ }
+
+ @Test
+ public void toStringMethod() throws Exception {
+ KeySequence keySequence = new KeySequence("Sample Key Sequence", KeyBindingTest.ALT_A, KeyBindingTest.SHIFT_B, KeyBindingTest.SAMPLE_EVENT);
+ assertEquals("Key sequence Sample Key Sequence Alt+A, Shift+B", keySequence.toString());
+ }
+
+ @Test
+ public void isIncluded() throws Exception {
+ KeySequence keySequence = new KeySequence("Sample Key Sequence", KeyBindingTest.ALT_A, KeyBindingTest.SHIFT_B, KeyBindingTest.SAMPLE_EVENT);
+
+ assertFalse(keySequence.isIncluded(null));
+
+ assertTrue(keySequence.isIncluded(KeyBindingTest.ALT_A));
+ assertTrue(keySequence.isIncluded(KeyBindingTest.SHIFT_B));
+
+ assertFalse(keySequence.isIncluded(KeyCodeCombination.valueOf("SHIFT + A")));
+ }
+
+ @Test
+ public void isElapsedTimePermissibile_validCases() throws Exception {
+
+ long baseTime = 32322;
+ long permissibleDelayInNanoSeconds =
+ NANOSECONDS.convert(KeySequence.KEY_SEQUENCE_MAX_MILLISECONDS_BETWEEN_KEYS, MILLISECONDS);
+
+ // Elapsed time is 0
+ assertTrue(KeySequence.isElapsedTimePermissibile(baseTime, baseTime));
+
+ // Elapsed time is exactly KeySequence.KEY_SEQUENCE_MAX_MILLISECONDS_BETWEEN_KEYS
+ assertTrue(KeySequence.isElapsedTimePermissibile(baseTime,
+ baseTime + permissibleDelayInNanoSeconds));
+
+ // Boundary case: Elapsed time is permitted time + 1
+ assertFalse(KeySequence.isElapsedTimePermissibile(baseTime,
+ baseTime + permissibleDelayInNanoSeconds + 1));
+
+ }
+ @Test
+ public void isElapsedTimePermissibile_negativeDuration_assertionError() throws Exception {
+ long baseTime = 32322;
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("second key event cannot happen before the first one");
+ KeySequence.isElapsedTimePermissibile(baseTime, baseTime-1);
+ }
+
+ @Test
+ public void isElapsedTimePermissibile_negativeFirstParam_assertionError() throws Exception {
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("times cannot be negative");
+ KeySequence.isElapsedTimePermissibile(-1, 0);
+ }
+
+ @Test
+ public void isElapsedTimePermissibile_negativeSecondParam_assertionError() throws Exception {
+ thrown.expect(AssertionError.class);
+ thrown.expectMessage("times cannot be negative");
+ KeySequence.isElapsedTimePermissibile(0, -1);
+ }
+}
diff --git a/src/test/java/address/keybindings/ShortcutTest.java b/src/test/java/address/keybindings/ShortcutTest.java
new file mode 100644
index 00000000..c3f03f89
--- /dev/null
+++ b/src/test/java/address/keybindings/ShortcutTest.java
@@ -0,0 +1,21 @@
+package address.keybindings;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+public class ShortcutTest {
+
+ Shortcut shortcut = new Shortcut("Dummy shortcut", KeyBindingTest.ALT_A, KeyBindingTest.SAMPLE_EVENT);
+
+ @Test
+ public void getKeyCombination() throws Exception {
+ assertEquals("Alt+A", shortcut.getKeyCombination().getDisplayText());
+ }
+
+ @Test
+ public void toStringMethod() throws Exception {
+ assertEquals("Keyboard shortcut Dummy shortcut Alt+A", shortcut.toString());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/address/model/AddPersonCommandTest.java b/src/test/java/address/model/AddPersonCommandTest.java
new file mode 100644
index 00000000..004936df
--- /dev/null
+++ b/src/test/java/address/model/AddPersonCommandTest.java
@@ -0,0 +1,204 @@
+package address.model;
+
+import address.events.CreatePersonOnRemoteRequestEvent;
+import address.model.ChangeObjectInModelCommand.State;
+import address.model.datatypes.person.Person;
+import address.model.datatypes.person.ReadOnlyPerson;
+import address.model.datatypes.person.ViewablePerson;
+import address.util.Config;
+import address.util.TestUtil;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AddPersonCommandTest {
+
+ public static final String ADDRESSBOOK_NAME = "ADDRESSBOOK NAME";
+
+ private static class InterruptAndTerminateException extends RuntimeException {}
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Mock
+ ModelManager modelManagerMock;
+ ModelManager modelManagerSpy;
+ @Mock
+ Config config;
+ EventBus events;
+
+ final Supplier> returnValidEmptyInput = () -> Optional.of(Person.createPersonDataContainer());
+
+ private static Supplier> inputRetrieverWrapper(ReadOnlyPerson inputValue) {
+ return () -> Optional.of(inputValue);
+ }
+
+ @BeforeClass
+ public static void beforeSetup() throws TimeoutException {
+ TestUtil.initRuntime();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
+ @Before
+ public void setup() {
+ when(config.getAddressBookName()).thenReturn(ADDRESSBOOK_NAME);
+ modelManagerSpy = spy(new ModelManager(config));
+ events = new EventBus();
+
+ events.register(new Object() {
+ @Subscribe
+ public void fakeAddToRemote(CreatePersonOnRemoteRequestEvent e) {
+ e.getReturnedPersonContainer().complete(e.getCreatedPerson());
+ }
+ });
+
+ }
+
+ @Test
+ public void getTargetPersonId_throws_whenViewableNotCreatedYet() {
+ final AddPersonCommand apc = new AddPersonCommand(0, null, 0, null, modelManagerMock, ADDRESSBOOK_NAME);
+ thrown.expect(IllegalStateException.class);
+ apc.getTargetPersonId();
+ }
+
+ @Test
+ public void getTargetPersonId_returnsTempId_afterResultSimulatedAndBeforeRemoteChange() {
+ final ViewablePerson createdViewable = ViewablePerson.withoutBacking(Person.createPersonDataContainer());
+ final int CORRECT_ID = createdViewable.getId();
+ final AddPersonCommand apc = spy(new AddPersonCommand(0, returnValidEmptyInput, 0, null, modelManagerMock, ADDRESSBOOK_NAME));
+
+ when(modelManagerMock.addViewablePersonWithoutBacking(notNull(ReadOnlyPerson.class))).thenReturn(createdViewable);
+
+ // to stop the run at start of grace period (right after simulated change)
+ doThrow(InterruptAndTerminateException.class).when(apc).beforeGracePeriod();
+ thrown.expect(InterruptAndTerminateException.class);
+
+ apc.run();
+ assertEquals(apc.getTargetPersonId(), CORRECT_ID);
+ }
+
+ @Test
+ public void getTargetPersonId_returnsFinalId_AfterSuccess() {
+ final int CORRECT_ID = 1;
+ events = new EventBus();
+ events.register(new Object() {
+ @Subscribe
+ public void fakeAddToRemote(CreatePersonOnRemoteRequestEvent e) {
+ e.getReturnedPersonContainer().complete(
+ new Person(CORRECT_ID).update(e.getCreatedPerson()));
+ }
+ });
+ final AddPersonCommand apc = new AddPersonCommand(0, returnValidEmptyInput, 0, events::post, modelManagerSpy, ADDRESSBOOK_NAME);
+
+ apc.run();
+ assertEquals(apc.getTargetPersonId(), CORRECT_ID);
+ }
+
+ @Test
+ public void retrievingInput_cancelsCommand_whenEmptyInputOptionalRetrieved() {
+ final AddPersonCommand apc = new AddPersonCommand(0, Optional::empty, 0, null, modelManagerMock, ADDRESSBOOK_NAME);
+ apc.run();
+ assertEquals(apc.getState(), State.CANCELLED);
+ }
+
+ @Test
+ public void optimisticUiUpdate_simulatesCorrectData() {
+ final ReadOnlyPerson inputData = TestUtil.generateSamplePersonWithAllData(0);
+ final AddPersonCommand apc = spy(new AddPersonCommand(0, inputRetrieverWrapper(inputData), 0, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ // to stop the run at start of grace period (right after simulated change)
+ doThrow(new InterruptAndTerminateException()).when(apc).beforeGracePeriod();
+ thrown.expect(InterruptAndTerminateException.class);
+
+ apc.run();
+
+ assertTrue(apc.getViewableToAdd().dataFieldsEqual(inputData)); // same data as input
+ assertEquals(modelManagerSpy.visibleModel().getPersonList().size(), 1); // only 1 viewable
+ assertTrue(modelManagerSpy.backingModel().getPersonList().isEmpty()); // simulation wont affect backing
+ assertSame(modelManagerSpy.visibleModel().getPersonList().get(0), apc.getViewableToAdd()); // same ref
+ }
+
+ @Test
+ public void successfulAdd_updatesBackingModelCorrectly() {
+ final ReadOnlyPerson inputData = TestUtil.generateSamplePersonWithAllData(0);
+ final AddPersonCommand apc = new AddPersonCommand(0, inputRetrieverWrapper(inputData), 0, events::post, modelManagerSpy, ADDRESSBOOK_NAME);
+
+ apc.run();
+ assertFalse(modelManagerSpy.personHasOngoingChange(apc.getViewableToAdd()));
+ assertFinalStatesCorrectForSuccessfulAdd(apc, modelManagerSpy, inputData);
+ }
+
+ // THIS TEST TAKES >=1 SECONDS BY DESIGN
+ @Test
+ public void interruptGracePeriod_withEditRequest_changesAddedPersonData() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final AddPersonCommand apc = spy(new AddPersonCommand(0, returnValidEmptyInput, 1, events::post, modelManagerSpy, ADDRESSBOOK_NAME));
+ final Supplier> editInputWrapper = inputRetrieverWrapper(TestUtil.generateSamplePersonWithAllData(1));
+
+ doNothing().when(apc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ apc.editInGracePeriod(editInputWrapper); // pre-specify apc will be interrupted by edit
+ apc.run();
+
+ assertFinalStatesCorrectForSuccessfulAdd(apc, modelManagerSpy, editInputWrapper.get().get());
+ }
+
+ @Test
+ public void interruptGracePeriod_withDeleteRequest_cancelsCommand() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final AddPersonCommand apc = spy(new AddPersonCommand(0, returnValidEmptyInput, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ doNothing().when(apc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ apc.deleteInGracePeriod(); // pre-specify apc will be interrupted by delete
+ apc.run();
+
+ assertTrue(modelManagerSpy.backingModel().getPersonList().isEmpty());
+ assertTrue(modelManagerSpy.visibleModel().getPersonList().isEmpty());
+ assertFalse(modelManagerSpy.personHasOngoingChange(apc.getViewableToAdd()));
+ assertEquals(apc.getState(), State.CANCELLED);
+ }
+
+ @Test
+ public void interruptGracePeriod_withCancelRequest_undoesSimulation() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final AddPersonCommand apc = spy(new AddPersonCommand(0, returnValidEmptyInput, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ doNothing().when(apc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ apc.cancelInGracePeriod(); // pre-specify apc will be interrupted by cancel
+ apc.run();
+
+ assertTrue(modelManagerSpy.backingModel().getPersonList().isEmpty());
+ assertTrue(modelManagerSpy.visibleModel().getPersonList().isEmpty());
+ assertFalse(modelManagerSpy.personHasOngoingChange(apc.getViewableToAdd()));
+ assertEquals(apc.getState(), State.CANCELLED);
+ }
+
+ private void assertFinalStatesCorrectForSuccessfulAdd(AddPersonCommand command, ModelManager model, ReadOnlyPerson resultData) {
+ assertEquals(command.getState(), State.SUCCESSFUL);
+ assertEquals(model.visibleModel().getPersonList().size(), 1); // only 1 viewable
+ assertEquals(model.backingModel().getPersonList().size(), 1); // only 1 backing
+
+ final ViewablePerson viewablePersonFromModel = model.visibleModel().getPersons().get(0);
+ final Person backingPersonFromModel = model.backingModel().getPersons().get(0);
+ assertSame(viewablePersonFromModel, command.getViewableToAdd()); // reference check
+ assertSame(viewablePersonFromModel.getBacking(), backingPersonFromModel); // backing connected properly to visible
+ assertTrue(viewablePersonFromModel.dataFieldsEqual(resultData));
+ assertTrue(backingPersonFromModel.dataFieldsEqual(resultData));
+ }
+
+}
diff --git a/src/test/java/address/model/DeletePersonCommandTest.java b/src/test/java/address/model/DeletePersonCommandTest.java
new file mode 100644
index 00000000..cae2ca70
--- /dev/null
+++ b/src/test/java/address/model/DeletePersonCommandTest.java
@@ -0,0 +1,151 @@
+package address.model;
+
+import address.events.DeletePersonOnRemoteRequestEvent;
+import address.model.ChangeObjectInModelCommand.State;
+import address.model.datatypes.person.ReadOnlyPerson;
+import address.model.datatypes.person.ReadOnlyViewablePerson;
+import address.model.datatypes.person.ViewablePerson;
+import address.util.Config;
+import address.util.TestUtil;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeletePersonCommandTest {
+
+ private static final String ADDRESSBOOK_NAME = "ADDRESSBOOK NAME";
+
+ private static class InterruptAndTerminateException extends RuntimeException {}
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Mock
+ ModelManager modelManagerMock;
+ ModelManager modelManagerSpy;
+ @Mock
+ Config config;
+ EventBus events;
+
+ public static final int TEST_ID = 314;
+ ViewablePerson testTarget;
+
+ @BeforeClass
+ public static void beforeSetup() throws TimeoutException {
+ TestUtil.initRuntime();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
+ @Before
+ public void setup() {
+ testTarget = spy(ViewablePerson.fromBacking(TestUtil.generateSamplePersonWithAllData(TEST_ID)));
+ when(config.getAddressBookName()).thenReturn(ADDRESSBOOK_NAME);
+ modelManagerSpy = spy(new ModelManager(config));
+ events = new EventBus();
+ events.register(new Object() {
+ @Subscribe
+ public void fakeDeleteOnRemote(DeletePersonOnRemoteRequestEvent e) {
+ e.getResultContainer().complete(true);
+ }
+ });
+ }
+
+ @Test
+ public void waitsForOtherOngoingCommandsOnTargetToFinish() throws InterruptedException {
+ final AddPersonCommand otherCommand = mock(AddPersonCommand.class);
+ when(modelManagerSpy.personHasOngoingChange(TEST_ID)).thenReturn(true);
+ when(modelManagerSpy.getOngoingChangeForPerson(TEST_ID)).thenReturn(otherCommand);
+
+ doThrow(InterruptAndTerminateException.class).when(otherCommand).waitForCompletion(); // don't actually wait
+ thrown.expect(InterruptAndTerminateException.class);
+
+ (new DeletePersonCommand(0, testTarget, 0, null, modelManagerSpy, ADDRESSBOOK_NAME)).run();
+
+ verify(otherCommand).waitForCompletion();
+ verify(modelManagerSpy, never()).assignOngoingChangeToPerson(any(), any());
+ }
+
+ @Test
+ public void getTargetPersonId_returnsCorrectId() {
+ final DeletePersonCommand epc = new DeletePersonCommand(0, testTarget, 0, null, modelManagerMock, ADDRESSBOOK_NAME);
+ assertEquals(epc.getTargetPersonId(), TEST_ID);
+ }
+
+ @Test
+ public void optimisticUiUpdate_flagsDelete() {
+ final DeletePersonCommand dpc = spy(new DeletePersonCommand(0, testTarget, 0, events::post, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ // to stop the run at start of grace period (right after simulated change)
+ doThrow(new InterruptAndTerminateException()).when(dpc).beforeGracePeriod();
+ thrown.expect(InterruptAndTerminateException.class);
+
+ dpc.run();
+ verify(testTarget).setChangeInProgress(ReadOnlyViewablePerson.ChangeInProgress.DELETING);
+ }
+
+ @Test
+ public void succesfulDelete_updatesBackingModelCorrectly() {
+ final DeletePersonCommand dpc = new DeletePersonCommand(0, testTarget, 0, events::post, modelManagerSpy, ADDRESSBOOK_NAME);
+
+ modelManagerSpy.visibleModel().addPerson(testTarget);
+ modelManagerSpy.addPersonToBackingModelSilently(testTarget.getBacking());
+ dpc.run();
+
+ assertTrue(modelManagerSpy.backingModel().getPersons().isEmpty());
+ assertTrue(modelManagerSpy.visibleModel().getPersons().isEmpty());
+ }
+
+ @Test
+ public void interruptGracePeriod_withEditRequest_cancelsAndSpawnsEditCommand() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final DeletePersonCommand dpc = spy(new DeletePersonCommand(0, testTarget, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+ final Supplier> editInputRetriever = Optional::empty;
+
+ doNothing().when(modelManagerSpy).execNewEditPersonCommand(any(), any());
+ doNothing().when(dpc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ dpc.editInGracePeriod(editInputRetriever); // pre-specify dpc will be interrupted by delete
+
+ dpc.run();
+
+ verify(modelManagerSpy).execNewEditPersonCommand(testTarget, editInputRetriever);
+ assertEquals(dpc.getState(), State.CANCELLED);
+ }
+
+ @Test
+ public void interruptGracePeriod_withCancelRequest_undoesSimulation() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final DeletePersonCommand dpc = spy(new DeletePersonCommand(0, testTarget, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+ final Supplier> editInputRetriever = Optional::empty;
+
+ modelManagerSpy.visibleModel().addPerson(testTarget);
+ modelManagerSpy.addPersonToBackingModelSilently(testTarget.getBacking());
+
+ doNothing().when(dpc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ dpc.cancelInGracePeriod(); // pre-specify dpc will be interrupted by cancel
+
+ dpc.run();
+
+ assertEquals(modelManagerSpy.backingModel().getPersonList().size(), 1);
+ assertEquals(modelManagerSpy.visibleModel().getPersonList().size(), 1);
+ assertSame(modelManagerSpy.visibleModel().getPersonList().get(0), testTarget);
+ assertSame(modelManagerSpy.backingModel().getPersonList().get(0), testTarget.getBacking());
+ assertEquals(testTarget.getChangeInProgress(), ReadOnlyViewablePerson.ChangeInProgress.NONE);
+ assertEquals(dpc.getState(), State.CANCELLED);
+ }
+}
diff --git a/src/test/java/address/model/EditPersonCommandTest.java b/src/test/java/address/model/EditPersonCommandTest.java
new file mode 100644
index 00000000..54264fe8
--- /dev/null
+++ b/src/test/java/address/model/EditPersonCommandTest.java
@@ -0,0 +1,174 @@
+package address.model;
+
+import address.events.UpdatePersonOnRemoteRequestEvent;
+import address.model.ChangeObjectInModelCommand.State;
+import address.model.datatypes.person.Person;
+import address.model.datatypes.person.ReadOnlyPerson;
+import address.model.datatypes.person.ViewablePerson;
+import address.util.Config;
+import address.util.TestUtil;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Optional;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EditPersonCommandTest {
+
+ private static final String ADDRESSBOOK_NAME = "ADDRESSBOOK NAME";
+
+ private static class InterruptAndTerminateException extends RuntimeException {}
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Mock
+ ModelManager modelManagerMock;
+ ModelManager modelManagerSpy;
+ @Mock
+ Config config;
+ EventBus events;
+
+ public static final int TEST_ID = 93;
+ ViewablePerson testTarget;
+ ReadOnlyPerson inputData = TestUtil.generateSamplePersonWithAllData(0);
+
+ final Supplier> returnValidEmptyInput = () -> Optional.of(Person.createPersonDataContainer());
+
+ private static Supplier> inputRetrieverWrapper(ReadOnlyPerson inputValue) {
+ return () -> Optional.of(inputValue);
+ }
+
+ @BeforeClass
+ public static void beforeSetup() throws TimeoutException {
+ TestUtil.initRuntime();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception {
+ TestUtil.tearDownRuntime();
+ }
+
+ @Before
+ public void setup() {
+ testTarget = ViewablePerson.fromBacking(TestUtil.generateSamplePersonWithAllData(TEST_ID));
+ when(config.getAddressBookName()).thenReturn(ADDRESSBOOK_NAME);
+ modelManagerSpy = spy(new ModelManager(config));
+ events = new EventBus();
+ events.register(new Object() {
+ @Subscribe
+ public void fakeAddToRemote(UpdatePersonOnRemoteRequestEvent e) {
+ e.getReturnedPersonContainer().complete(e.getUpdatedPerson());
+ }
+ });
+ }
+
+ @Test
+ public void waitsForOtherOngoingCommandsOnTargetToFinish() throws InterruptedException {
+ final AddPersonCommand otherCommand = mock(AddPersonCommand.class);
+ when(modelManagerSpy.personHasOngoingChange(TEST_ID)).thenReturn(true);
+ when(modelManagerSpy.getOngoingChangeForPerson(TEST_ID)).thenReturn(otherCommand);
+
+ doThrow(InterruptAndTerminateException.class).when(otherCommand).waitForCompletion(); // don't actually wait
+ thrown.expect(InterruptAndTerminateException.class);
+
+ (new EditPersonCommand(0, testTarget, returnValidEmptyInput, 0, null, modelManagerSpy, ADDRESSBOOK_NAME)).run();
+
+ verify(otherCommand).waitForCompletion();
+ verify(modelManagerSpy, never()).assignOngoingChangeToPerson(any(), any());
+ }
+
+ @Test
+ public void getTargetPersonId_returnsCorrectId() {
+ final EditPersonCommand epc = new EditPersonCommand(0, testTarget, null, 0, null, modelManagerMock, ADDRESSBOOK_NAME);
+ assertEquals(epc.getTargetPersonId(), TEST_ID);
+ }
+
+ @Test
+ public void retrievingInput_cancelsCommand_whenEmptyInputOptionalRetrieved() {
+ final EditPersonCommand epc = new EditPersonCommand(0, testTarget, Optional::empty, 0, null, modelManagerMock, ADDRESSBOOK_NAME);
+ epc.run();
+ assertEquals(epc.getState(), State.CANCELLED);
+ }
+
+ @Test
+ public void optimisticUiUpdate_simulatesCorrectData() {
+ final EditPersonCommand epc = spy(new EditPersonCommand(0, testTarget, inputRetrieverWrapper(inputData),
+ 0, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ // to stop the run at start of grace period (right after simulated change)
+ doThrow(new InterruptAndTerminateException()).when(epc).beforeGracePeriod();
+ thrown.expect(InterruptAndTerminateException.class);
+
+ epc.run();
+
+ assertTrue(epc.getViewable().dataFieldsEqual(inputData)); // same data as input
+ assertEquals(modelManagerSpy.visibleModel().getPersonList().size(), 1); // only 1 viewable
+ assertTrue(modelManagerSpy.backingModel().getPersonList().isEmpty()); // simulation wont affect backing
+ assertSame(modelManagerSpy.visibleModel().getPersonList().get(0), epc.getViewable()); // same ref
+ }
+
+ @Test
+ public void succesfulEdit_updatesBackingModelCorrectly() {
+ final EditPersonCommand epc = new EditPersonCommand(0, testTarget, inputRetrieverWrapper(inputData),
+ 0, events::post, modelManagerSpy, ADDRESSBOOK_NAME);
+ epc.run();
+ assertTrue(epc.getViewable().dataFieldsEqual(inputData));
+ assertTrue(epc.getViewable().getBacking().dataFieldsEqual(inputData));
+ }
+
+ // THIS TEST TAKES >=1 SECONDS BY DESIGN
+ @Test
+ public void interruptGracePeriod_withEditRequest_changesEditResult() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final EditPersonCommand epc = spy(new EditPersonCommand(0, testTarget, returnValidEmptyInput, 1, events::post, modelManagerSpy, ADDRESSBOOK_NAME));
+ final Supplier> editInputWrapper = inputRetrieverWrapper(inputData);
+
+ doNothing().when(epc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ epc.editInGracePeriod(editInputWrapper); // pre-specify apc will be interrupted by edit
+ epc.run();
+
+ assertTrue(epc.getViewable().dataFieldsEqual(inputData));
+ assertTrue(epc.getViewable().getBacking().dataFieldsEqual(inputData));
+ }
+
+ @Test
+ public void interruptGracePeriod_withDeleteRequest_cancelsAndSpawnsDeleteCommand() {
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final EditPersonCommand epc = spy(new EditPersonCommand(0, testTarget, returnValidEmptyInput, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ doNothing().when(modelManagerSpy).execNewDeletePersonCommand(any());
+ doNothing().when(epc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ epc.deleteInGracePeriod(); // pre-specify epc will be interrupted by delete
+
+ epc.run();
+
+ verify(modelManagerSpy).execNewDeletePersonCommand(testTarget);
+ assertEquals(epc.getState(), State.CANCELLED);
+ }
+
+ @Test
+ public void interruptGracePeriod_withCancelRequest_undoesSimulation() {
+ final ReadOnlyPerson targetSnapshot = new Person(testTarget);
+ // grace period duration must be non zero, will be interrupted immediately anyway
+ final EditPersonCommand epc = spy(new EditPersonCommand(0, testTarget, returnValidEmptyInput, 1, null, modelManagerSpy, ADDRESSBOOK_NAME));
+
+ doNothing().when(epc).beforeGracePeriod(); // don't wipe interrupt code injection when grace period starts
+ epc.cancelInGracePeriod(); // pre-specify epc will be interrupted by cancel
+
+ epc.run();
+
+ assertTrue(epc.getViewable().dataFieldsEqual(targetSnapshot));
+ assertEquals(epc.getState(), State.CANCELLED);
+ }
+}
diff --git a/src/test/java/address/model/ModelManagerTest.java b/src/test/java/address/model/ModelManagerTest.java
new file mode 100644
index 00000000..14ab65b0
--- /dev/null
+++ b/src/test/java/address/model/ModelManagerTest.java
@@ -0,0 +1,47 @@
+package address.model;
+
+import address.model.datatypes.person.ReadOnlyPerson;
+import address.util.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ModelManagerTest {
+
+ @Mock
+ ModelManager modelMock;
+ @Mock
+ Config config;
+ ModelManager modelSpy;
+
+ @Before
+ public void setup() {
+ when(config.getLocalDataFilePath()).thenReturn("MyAddressBook");
+ modelSpy = spy(new ModelManager(config));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void createPersonThroughUi_spawnsNewAddCommand() {
+ doCallRealMethod().when(modelMock).createPersonThroughUI(any());
+ Callable> addInputRetriever = Optional::empty;
+ modelMock.createPersonThroughUI(addInputRetriever);
+ verify(modelMock).execNewAddPersonCommand(notNull(Supplier.class));
+ }
+
+ @Test
+ public void editPersonThroughUi_spawnsNewEditCommand_ifTargetHasNoChangeInProgress() {
+// doCallRealMethod().when(modelMock).editPersonThroughUI(any());
+
+ }
+}
diff --git a/src/test/java/address/unittests/PersonEditDialogModelTest.java b/src/test/java/address/model/PersonEditDialogModelTest.java
similarity index 97%
rename from src/test/java/address/unittests/PersonEditDialogModelTest.java
rename to src/test/java/address/model/PersonEditDialogModelTest.java
index dbc07280..206bcc9e 100644
--- a/src/test/java/address/unittests/PersonEditDialogModelTest.java
+++ b/src/test/java/address/model/PersonEditDialogModelTest.java
@@ -1,4 +1,4 @@
-package address.unittests;
+package address.model;
import address.model.datatypes.tag.Tag;
import address.model.TagSelectionEditDialogModel;
diff --git a/src/test/java/address/unittests/parser/ParserTest.java b/src/test/java/address/parser/ParserTest.java
similarity index 99%
rename from src/test/java/address/unittests/parser/ParserTest.java
rename to src/test/java/address/parser/ParserTest.java
index d0bbb117..16431d12 100644
--- a/src/test/java/address/unittests/parser/ParserTest.java
+++ b/src/test/java/address/parser/ParserTest.java
@@ -1,4 +1,4 @@
-package address.unittests.parser;
+package address.parser;
import address.model.datatypes.person.ReadOnlyViewablePerson;
import address.model.datatypes.tag.Tag;
diff --git a/src/test/java/address/unittests/storage/StorageManagerTest.java b/src/test/java/address/storage/StorageManagerTest.java
similarity index 87%
rename from src/test/java/address/unittests/storage/StorageManagerTest.java
rename to src/test/java/address/storage/StorageManagerTest.java
index bc9f3dfe..41cde121 100644
--- a/src/test/java/address/unittests/storage/StorageManagerTest.java
+++ b/src/test/java/address/storage/StorageManagerTest.java
@@ -1,14 +1,10 @@
-package address.unittests.storage;
+package address.storage;
import address.events.*;
import address.exceptions.DataConversionException;
import address.model.ModelManager;
import address.model.UserPrefs;
import address.model.datatypes.AddressBook;
-import address.model.datatypes.ReadOnlyAddressBook;
-import address.storage.StorageAddressBook;
-import address.storage.StorageManager;
-import address.storage.XmlFileStorage;
import address.util.Config;
import address.util.FileUtil;
import address.util.SerializableTestClass;
@@ -18,6 +14,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@@ -32,13 +29,13 @@
@RunWith(PowerMockRunner.class)
@PrepareForTest({XmlFileStorage.class, FileUtil.class})
+@PowerMockIgnore({"javax.management.*"})// Defer loading of javax.management.* in log4j to system class loader
public class StorageManagerTest {
-
+ private static final String DUMMY_DATA_FILE_PATH = TestUtil.appendToSandboxPath("dummyAddressBook.xml");
private static final File DUMMY_DATA_FILE = new File(TestUtil.appendToSandboxPath("dummyAddressBook.xml"));
private static final File DUMMY_PREFS_FILE = new File(TestUtil.appendToSandboxPath("dummyUserPrefs.json"));
private static final File SERIALIZATION_FILE = new File(TestUtil.appendToSandboxPath("serialize.json"));
private static final File INEXISTENT_FILE = new File(TestUtil.appendToSandboxPath("inexistent"));
- private static final File NON_JSON_FILE = new File(TestUtil.appendToSandboxPath("non.json"));
private static final File EMPTY_FILE = new File(TestUtil.appendToSandboxPath("empty.json"));
private static final StorageAddressBook EMPTY_ADDRESSBOOK = new StorageAddressBook(new AddressBook());
@@ -52,19 +49,16 @@ public class StorageManagerTest {
StorageManager storageManagerSpy;
@Before
- public void setup(){
-
+ public void setup() {
//mock the dependent static class
PowerMockito.mockStatic(XmlFileStorage.class);
//create mocks for dependencies and inject them into StorageManager object under test
- userPrefsMock = Mockito.mock(UserPrefs.class);
- when(userPrefsMock.getSaveLocation()).thenReturn(DUMMY_DATA_FILE);
- modelManagerMock = Mockito.mock(ModelManager.class);
- when(modelManagerMock.getPrefs()).thenReturn(userPrefsMock);
eventManagerMock = Mockito.mock(EventManager.class);
configMock = Mockito.mock(Config.class);
when(configMock.getPrefsFileLocation()).thenReturn(DUMMY_PREFS_FILE);
+ when(configMock.getLocalDataFilePath()).thenReturn(DUMMY_DATA_FILE_PATH);
+ modelManagerMock = Mockito.mock(ModelManager.class);
storageManager = new StorageManager(modelManagerMock::resetData, configMock, userPrefsMock);
storageManager.setEventManager(eventManagerMock);
@@ -78,17 +72,17 @@ public void getConfig_fileInexistent_newConfigCreated() throws Exception {
Config dummyConfig = new Config();
FileUtil.deleteFileIfExists(INEXISTENT_FILE);
assertFalse(INEXISTENT_FILE.exists());
- TestUtil.setFinalStatic(StorageManager.class.getDeclaredField("CONFIG_FILE"), INEXISTENT_FILE.getPath());
+ TestUtil.setFinalStatic(StorageManager.class.getDeclaredField("DEFAULT_CONFIG_FILE"), INEXISTENT_FILE.getPath());
PowerMockito.whenNew(Config.class).withAnyArguments().thenReturn(dummyConfig);
- StorageManager.getConfig();
+ StorageManager.getConfig(null);
PowerMockito.verifyNew(Config.class);
}
@Test
public void getConfig_fileExistent_correspondingMethodCalled() throws Exception {
FileUtil.createIfMissing(EMPTY_FILE);
- TestUtil.setFinalStatic(StorageManager.class.getDeclaredField("CONFIG_FILE"), EMPTY_FILE.getPath());
- StorageManager.getConfig();
+ TestUtil.setFinalStatic(StorageManager.class.getDeclaredField("DEFAULT_CONFIG_FILE"), EMPTY_FILE.getPath());
+ StorageManager.getConfig(null);
PowerMockito.verifyPrivate(StorageManager.class).invoke("readFromConfigFile", EMPTY_FILE);
}
@@ -128,7 +122,7 @@ public void getData_correspondingMethodCalled() throws FileNotFoundException, Da
@Test
public void start_correspondingMethodCalled() throws Exception {
storageManagerSpy.start();
- PowerMockito.verifyPrivate(storageManagerSpy).invoke("loadDataFromFile", userPrefsMock.getSaveLocation());
+ PowerMockito.verifyPrivate(storageManagerSpy).invoke("loadDataFromFile", new File(configMock.getLocalDataFilePath()));
}
@Test
@@ -222,21 +216,6 @@ public void handleSavePrefsRequestEvent(){
verify(storageManagerSpy, times(1)).savePrefsToFile(EMPTY_USERPREFS);
}
- @Test
- public void getUserPrefs_inexistentFile_emptyUserPrefs() throws IOException {
- UserPrefs emptyUserPrefs = new UserPrefs();
- UserPrefs fromInexistentFile = StorageManager.getUserPrefs(INEXISTENT_FILE);
- assertEquals(emptyUserPrefs.getSaveLocation(), fromInexistentFile.getSaveLocation());
- }
-
- @Test
- public void getUserPrefs_nonJsonFile_emptyUserPrefs() throws IOException {
- FileUtil.writeToFile(NON_JSON_FILE, "}");
- UserPrefs emptyUserPrefs = new UserPrefs();
- UserPrefs fromInexistentFile = StorageManager.getUserPrefs(NON_JSON_FILE);
- assertEquals(emptyUserPrefs.getSaveLocation(), fromInexistentFile.getSaveLocation());
- }
-
@Test
public void serializeObjectToJsonFile_noExceptionThrown() throws IOException {
SerializableTestClass serializableTestClass = new SerializableTestClass();
diff --git a/src/test/java/address/unittests/storage/XmlFileStorageTest.java b/src/test/java/address/storage/XmlFileStorageTest.java
similarity index 92%
rename from src/test/java/address/unittests/storage/XmlFileStorageTest.java
rename to src/test/java/address/storage/XmlFileStorageTest.java
index 601dfc87..f6eab574 100644
--- a/src/test/java/address/unittests/storage/XmlFileStorageTest.java
+++ b/src/test/java/address/storage/XmlFileStorageTest.java
@@ -1,8 +1,6 @@
-package address.unittests.storage;
+package address.storage;
import address.model.datatypes.AddressBook;
-import address.storage.StorageAddressBook;
-import address.storage.XmlFileStorage;
import address.util.XmlUtil;
import org.junit.Before;
import org.junit.Test;
diff --git a/src/test/java/address/unittests/sync/RemoteManagerTest.java b/src/test/java/address/sync/RemoteManagerTest.java
similarity index 97%
rename from src/test/java/address/unittests/sync/RemoteManagerTest.java
rename to src/test/java/address/sync/RemoteManagerTest.java
index f626794d..d7a49a4d 100644
--- a/src/test/java/address/unittests/sync/RemoteManagerTest.java
+++ b/src/test/java/address/sync/RemoteManagerTest.java
@@ -1,10 +1,7 @@
-package address.unittests.sync;
+package address.sync;
import address.model.datatypes.person.Person;
import address.model.datatypes.tag.Tag;
-import address.sync.ExtractedRemoteResponse;
-import address.sync.RemoteManager;
-import address.sync.RemoteService;
import org.junit.Before;
import org.junit.Test;
diff --git a/src/test/java/address/unittests/sync/RemoteServiceTest.java b/src/test/java/address/sync/RemoteServiceTest.java
similarity index 68%
rename from src/test/java/address/unittests/sync/RemoteServiceTest.java
rename to src/test/java/address/sync/RemoteServiceTest.java
index 5536c948..f625cf8e 100644
--- a/src/test/java/address/unittests/sync/RemoteServiceTest.java
+++ b/src/test/java/address/sync/RemoteServiceTest.java
@@ -1,11 +1,10 @@
-package address.unittests.sync;
+package address.sync;
import address.model.datatypes.person.Person;
import address.model.datatypes.tag.Tag;
+import address.sync.cloud.CloudRateLimitStatus;
import address.sync.cloud.RemoteResponse;
-import address.sync.RemoteService;
import address.sync.cloud.CloudSimulator;
-import address.sync.ExtractedRemoteResponse;
import address.sync.cloud.model.CloudPerson;
import address.sync.cloud.model.CloudTag;
import org.junit.Before;
@@ -31,12 +30,12 @@ public class RemoteServiceTest {
private RemoteService remoteService;
private CloudSimulator cloudSimulator;
- private HashMap getHeader(int limit, int remaining, long reset) {
- HashMap headers = new HashMap<>();
- headers.put("X-RateLimit-Limit", String.valueOf(limit));
- headers.put("X-RateLimit-Remaining", String.valueOf(remaining));
- headers.put("X-RateLimit-Reset", String.valueOf(reset));
- return headers;
+ private CloudRateLimitStatus getCloudRateLimitStatus(int limit, int remaining, long reset) {
+ return new CloudRateLimitStatus(limit, remaining, reset);
+ }
+
+ private CloudRateLimitStatus getCloudRateLimitStatus(int limit, int remaining) {
+ return getCloudRateLimitStatus(limit, remaining, getResetTime());
}
private ZoneOffset getSystemTimezone() {
@@ -54,14 +53,14 @@ public void setup() {
@Test
public void getPersons() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
List personsToReturn = new ArrayList<>();
CloudPerson personToReturn = new CloudPerson("firstName", "lastName");
personToReturn.setId(1);
personsToReturn.add(personToReturn);
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, personsToReturn, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, personsToReturn, cloudRateLimitStatus, null);
when(cloudSimulator.getPersons("Test", 1, RESOURCES_PER_PAGE, null)).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getPersons("Test", 1);
@@ -69,20 +68,22 @@ public void getPersons() throws IOException {
assertEquals(1, serviceResponse.getData().get().size());
assertEquals("firstName", serviceResponse.getData().get().get(0).getFirstName());
assertEquals("lastName", serviceResponse.getData().get().get(0).getLastName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void getPersons_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 10;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.getPersons("Test", 1, RESOURCES_PER_PAGE, null)).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getPersons("Test", 1);
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
private long getResetTime() {
@@ -92,11 +93,11 @@ private long getResetTime() {
@Test
public void createPerson() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
CloudPerson remotePerson = new CloudPerson("unknownName", "unknownName");
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remotePerson, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remotePerson, cloudRateLimitStatus, null);
when(cloudSimulator.createPerson(anyString(), any(CloudPerson.class), isNull(String.class))).thenReturn(remoteResponse);
Person person = new Person("unknownName", "unknownName", 0);
@@ -105,12 +106,15 @@ public void createPerson() throws IOException {
assertEquals(HttpURLConnection.HTTP_CREATED, serviceResponse.getResponseCode());
assertTrue(serviceResponse.getData().isPresent());
assertEquals(person, serviceResponse.getData().get());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void createPerson_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 10;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.createPerson(anyString(), any(CloudPerson.class), isNull(String.class))).thenReturn(remoteResponse);
Person person = new Person("unknownName", "unknownName", 0);
@@ -118,22 +122,21 @@ public void createPerson_errorCloudResponse() throws IOException {
ExtractedRemoteResponse serviceResponse = remoteService.createPerson("Test", person);
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void createPerson_newTag_successfulCreationOfPersonAndTag() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
CloudPerson remotePerson = new CloudPerson("unknownName", "unknownName");
List personCloudTags = new ArrayList<>();
personCloudTags.add(new CloudTag("New Tag"));
remotePerson.setTags(personCloudTags);
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remotePerson, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remotePerson, cloudRateLimitStatus, null);
when(cloudSimulator.createPerson(anyString(), any(CloudPerson.class), isNull(String.class))).thenReturn(remoteResponse);
Person person = new Person("unknownName", "unknownName", 0);
@@ -148,17 +151,17 @@ public void createPerson_newTag_successfulCreationOfPersonAndTag() throws IOExce
assertEquals(person, serviceResponse.getData().get());
assertEquals(person.getTagList().size(), serviceResponse.getData().get().getObservableTagList().size());
assertEquals(person.getTagList().get(0).getName(), serviceResponse.getData().get().getObservableTagList().get(0).getName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void createTag() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
CloudTag remoteTag = new CloudTag("New Tag");
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remoteTag, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, remoteTag, cloudRateLimitStatus, null);
when(cloudSimulator.createTag(anyString(), any(CloudTag.class), isNull(String.class))).thenReturn(remoteResponse);
Tag tag = new Tag("New Tag");
@@ -167,12 +170,15 @@ public void createTag() throws IOException {
assertEquals(HttpURLConnection.HTTP_CREATED, serviceResponse.getResponseCode());
assertTrue(serviceResponse.getData().isPresent());
assertEquals(tag, serviceResponse.getData().get());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void createTag_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 10;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.createTag(anyString(), any(CloudTag.class), isNull(String.class))).thenReturn(remoteResponse);
Tag tag = new Tag("New Tag");
@@ -180,21 +186,20 @@ public void createTag_errorCloudResponse() throws IOException {
ExtractedRemoteResponse serviceResponse = remoteService.createTag("Test", tag);
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void getTags() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
List tagList = new ArrayList<>();
tagList.add(new CloudTag("tagName"));
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, tagList, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, tagList, cloudRateLimitStatus, null);
when(cloudSimulator.getTags(anyString(), anyInt(), anyInt(), isNull(String.class))).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getTags("Test", 1, null);
@@ -203,19 +208,19 @@ public void getTags() throws IOException {
assertTrue(serviceResponse.getData().isPresent());
assertEquals(1, serviceResponse.getData().get().size());
assertEquals("tagName", serviceResponse.getData().get().get(0).getName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void updatePerson() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
CloudPerson remotePerson = new CloudPerson("newFirstName", "newLastName");
remotePerson.setId(1);
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remotePerson, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remotePerson, cloudRateLimitStatus, null);
when(cloudSimulator.updatePerson(anyString(), anyInt(), any(CloudPerson.class), isNull(String.class))).thenReturn(remoteResponse);
Person updatedPerson = new Person("newFirstName", "newLastName", 1);
@@ -225,12 +230,15 @@ public void updatePerson() throws IOException {
assertTrue(serviceResponse.getData().isPresent());
assertEquals("newFirstName", serviceResponse.getData().get().getFirstName());
assertEquals("newLastName", serviceResponse.getData().get().getLastName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void updatePerson_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 10;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.updatePerson(anyString(), anyInt(), any(CloudPerson.class), isNull(String.class))).thenReturn(remoteResponse);
Person updatedPerson = new Person("newFirstName", "newLastName", 1);
@@ -238,19 +246,18 @@ public void updatePerson_errorCloudResponse() throws IOException {
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void editTag() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
CloudTag remoteTag = new CloudTag("newTagName");
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remoteTag, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remoteTag, cloudRateLimitStatus, null);
when(cloudSimulator.editTag(anyString(), anyString(), any(CloudTag.class), isNull(String.class))).thenReturn(remoteResponse);
Tag updatedTag = new Tag("newTagName");
@@ -259,12 +266,15 @@ public void editTag() throws IOException {
assertEquals(HttpURLConnection.HTTP_OK, serviceResponse.getResponseCode());
assertTrue(serviceResponse.getData().isPresent());
assertEquals("newTagName", serviceResponse.getData().get().getName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void editTag_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 5;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.editTag(anyString(), anyString(), any(CloudTag.class), isNull(String.class))).thenReturn(remoteResponse);
Tag updatedTag = new Tag("newTagName");
@@ -272,98 +282,101 @@ public void editTag_errorCloudResponse() throws IOException {
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void deletePerson() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 3;
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_NO_CONTENT, null, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_NO_CONTENT, null, cloudRateLimitStatus, null);
when(cloudSimulator.deletePerson("Test", 1)).thenReturn(remoteResponse);
ExtractedRemoteResponse serviceResponse = remoteService.deletePerson("Test", 1);
assertEquals(HttpURLConnection.HTTP_NO_CONTENT, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void deletePerson_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 8;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.deletePerson("Test", 1)).thenReturn(remoteResponse);
ExtractedRemoteResponse serviceResponse = remoteService.deletePerson("Test", 1);
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void deleteTag() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 10;
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_NO_CONTENT, null, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_NO_CONTENT, null, cloudRateLimitStatus, null);
when(cloudSimulator.deleteTag("Test", "tagName")).thenReturn(remoteResponse);
ExtractedRemoteResponse serviceResponse = remoteService.deleteTag("Test", "tagName");
assertEquals(HttpURLConnection.HTTP_NO_CONTENT, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void deleteTag_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 9;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.deleteTag("Test", "tagName")).thenReturn(remoteResponse);
ExtractedRemoteResponse serviceResponse = remoteService.deleteTag("Test", "tagName");
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void createAddressBook() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 2;
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, null, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_CREATED, null, cloudRateLimitStatus, null);
when(cloudSimulator.createAddressBook("Test")).thenReturn(remoteResponse);
ExtractedRemoteResponse serviceResponse = remoteService.createAddressBook("Test");
assertEquals(HttpURLConnection.HTTP_CREATED, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void getUpdatedPersonsSince() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 9;
+ int quotaRemaining = 1;
LocalDateTime cutOffTime = LocalDateTime.now();
List remotePersons = new ArrayList<>();
remotePersons.add(new CloudPerson("firstName", "lastName"));
- HashMap header = getHeader(quotaLimit, quotaRemaining, getResetTime());
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remotePersons, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, remotePersons, cloudRateLimitStatus, null);
when(cloudSimulator.getUpdatedPersons(anyString(), anyString(), anyInt(), anyInt(), anyString())).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getUpdatedPersonsSince("Test", 1, cutOffTime, null);
@@ -373,33 +386,35 @@ public void getUpdatedPersonsSince() throws IOException {
assertEquals(1, serviceResponse.getData().get().size());
assertEquals("firstName", serviceResponse.getData().get().get(0).getFirstName());
assertEquals("lastName", serviceResponse.getData().get().get(0).getLastName());
- assertEquals(quotaRemaining, serviceResponse.getQuotaRemaining());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void getUpdatedPersonsSince_errorCloudResponse_returnEmptyResponse() throws IOException {
- LocalDateTime cutOffTime = LocalDateTime.now();
+ int quotaLimit = 10;
+ int quotaRemaining = 4;
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ LocalDateTime cutOffTime = LocalDateTime.now();
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.getUpdatedPersons(anyString(), anyString(), anyInt(), anyInt(), anyString())).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getUpdatedPersonsSince("Test", 1, cutOffTime, null);
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
@Test
public void getLimitStatus() throws IOException {
int quotaLimit = 10;
- int quotaRemaining = 10;
+ int quotaRemaining = 8;
long resetTime = getResetTime();
- HashMap header = getHeader(quotaLimit, quotaRemaining, resetTime);
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_OK, header, header);
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining, resetTime);
+ RemoteResponse remoteResponse = RemoteResponse.getLimitStatusResponse(cloudRateLimitStatus);
when(cloudSimulator.getRateLimitStatus(isNull(String.class))).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getLimitStatus();
@@ -407,20 +422,22 @@ public void getLimitStatus() throws IOException {
assertTrue(serviceResponse.getData().isPresent());
assertEquals(3, serviceResponse.getData().get().size());
assertEquals("10", serviceResponse.getData().get().get("Limit"));
- assertEquals("10", serviceResponse.getData().get().get("Remaining"));
+ assertEquals("8", serviceResponse.getData().get().get("Remaining"));
assertEquals(String.valueOf(resetTime), serviceResponse.getData().get().get("Reset"));
}
@Test
public void getLimitStatus_errorCloudResponse() throws IOException {
- RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
+ int quotaLimit = 10;
+ int quotaRemaining = 9;
+ CloudRateLimitStatus cloudRateLimitStatus = getCloudRateLimitStatus(quotaLimit, quotaRemaining);
+ RemoteResponse remoteResponse = new RemoteResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, null, cloudRateLimitStatus, null);
when(cloudSimulator.getRateLimitStatus(isNull(String.class))).thenReturn(remoteResponse);
ExtractedRemoteResponse> serviceResponse = remoteService.getLimitStatus();
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, serviceResponse.getResponseCode());
assertFalse(serviceResponse.getData().isPresent());
- assertEquals(0, serviceResponse.getQuotaLimit());
- assertEquals(0, serviceResponse.getQuotaRemaining());
- assertNull(serviceResponse.getQuotaResetTime());
+ assertEquals(quotaLimit, serviceResponse.getQuotaLimit());
+ assertEquals(quotaRemaining - 1, serviceResponse.getQuotaRemaining());
}
}
diff --git a/src/test/java/address/unittests/sync/SyncManagerTest.java b/src/test/java/address/sync/SyncManagerTest.java
similarity index 93%
rename from src/test/java/address/unittests/sync/SyncManagerTest.java
rename to src/test/java/address/sync/SyncManagerTest.java
index b608bb89..78cdcc82 100644
--- a/src/test/java/address/unittests/sync/SyncManagerTest.java
+++ b/src/test/java/address/sync/SyncManagerTest.java
@@ -1,12 +1,10 @@
-package address.unittests.sync;
+package address.sync;
import address.events.CreatePersonOnRemoteRequestEvent;
import address.events.EventManager;
import address.events.SyncFailedEvent;
import address.model.datatypes.person.Person;
import address.model.datatypes.person.ReadOnlyPerson;
-import address.sync.RemoteManager;
-import address.sync.SyncManager;
import address.sync.task.CreatePersonOnRemoteTask;
import address.util.Config;
import com.google.common.eventbus.Subscribe;
@@ -14,7 +12,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@@ -30,6 +28,7 @@
@RunWith(PowerMockRunner.class)
@PrepareForTest({CreatePersonOnRemoteTask.class, SyncManager.class})
+@PowerMockIgnore({"javax.management.*"})// Defer loading of javax.management.* in log4j to system class loader
public class SyncManagerTest {
RemoteManager remoteManager;
SyncManager syncManager;
@@ -52,8 +51,7 @@ public void setup() {
executorService = spy(Executors.newCachedThreadPool());
scheduledExecutorService = mock(ScheduledExecutorService.class);
config = new Config();
- config.simulateUnreliableNetwork = false;
- config.updateInterval = 1;
+ config.setUpdateInterval(1);
syncManager = new SyncManager(config, remoteManager, executorService, scheduledExecutorService, null);
}
diff --git a/src/test/java/address/sync/cloud/CloudManipulator.java b/src/test/java/address/sync/cloud/CloudManipulator.java
new file mode 100644
index 00000000..e2d207ac
--- /dev/null
+++ b/src/test/java/address/sync/cloud/CloudManipulator.java
@@ -0,0 +1,532 @@
+package address.sync.cloud;
+
+import address.exceptions.DataConversionException;
+import address.sync.cloud.model.CloudAddressBook;
+import address.sync.cloud.model.CloudPerson;
+import address.sync.cloud.model.CloudTag;
+import address.util.AppLogger;
+import address.util.Config;
+import address.util.LoggerManager;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Adds data/response manipulation features to the CloudSimulator
+ *
+ * Launches a GUI for the tester to simulate errors and data modifications
+ * By default, the responses returned should be same as the ones returned from CloudSimulator, with little to no delay
+ *
+ * The CloudManipulator will attempt to initialize a cloud file with cloud address book data.
+ * This data can be provided through the following means:
+ * - A cloud address book can be provided for initialization.
+ * - A cloud data file path can be provided in config. If reading fails, it will initialize an empty cloud file
+ * with the given address book name in config.
+ */
+public class CloudManipulator extends CloudSimulator {
+ private static final AppLogger logger = LoggerManager.getLogger(CloudManipulator.class);
+ private static final String DELAY_BUTTON_TEXT = "Delay next response";
+ private static final String FAIL_BUTTON_TEXT = "Fail next response";
+ private static final String DELAY_BUTTON_ICON_PATH = "/images/clock.png";
+ private static final String FAIL_BUTTON_ICON_PATH = "/images/fail.png";
+ private static final String ADD_PERSON_TEXT = "Add person";
+ private static final String MODIFY_PERSON_TEXT = "Modify person";
+ private static final String DELETE_PERSON_TEXT = "Delete person";
+ private static final String ADD_TAG_TEXT = "Add tag";
+ private static final String MODIFY_TAG_TEXT = "Modify tag";
+ private static final String DELETE_TAG_TEXT = "Delete tag";
+ private static final String CLOUD_MANIPULATOR_TITLE = "Cloud Manipulator";
+ private static final String ADDRESS_BOOK_FIELD_TOOLTIP_TEXT = "Enter address book to target.";
+ private static final String ADDRESS_BOOK_FIELD_PROMPT_TEXT = "Address Book";
+ private static final String FAIL_SYNC_UP_CHECKBOX_TEXT = "Fail all Sync-Up requests";
+ private static final String FAIL_SYNC_DOWN_CHECKBOX_TEXT = "Fail all Sync-Down requests";
+ private static final int CONSOLE_WIDTH = 300;
+ private static final int CONSOLE_HEIGHT = 600;
+
+ private static final Random RANDOM_GENERATOR = new Random();
+ private static final int MIN_DELAY_IN_SEC = 1;
+ private static final int MAX_DELAY_IN_SEC = 5;
+
+ private SimpleBooleanProperty shouldDelayNext = new SimpleBooleanProperty(false);
+ private SimpleBooleanProperty shouldFailNext = new SimpleBooleanProperty(false);
+
+ private String addressBookName;
+ private TextArea statusArea;
+ private TextField addressBookField;
+ private CheckBox failSyncUpsCheckBox;
+ private CheckBox failSyncDownsCheckBox;
+
+ /**
+ * Initializes CloudManipulator with data found in config's cloudDataFilePath
+ * Upon failure, it will initialize an empty address book with config's addressBookName
+ *
+ * @param config
+ */
+ public CloudManipulator(Config config) {
+ super(config);
+ if (config.getCloudDataFilePath() != null) {
+ initializeCloudFile(config.getCloudDataFilePath(), config.getAddressBookName());
+ } else {
+ initializeCloudFile(new CloudAddressBook(config.getAddressBookName()));
+ }
+ }
+
+ /**
+ * Initializes CloudManipulator with the provided cloud address book
+ *
+ * @param config
+ */
+ public CloudManipulator(Config config, CloudAddressBook cloudAddressBook) {
+ super(config);
+ initializeCloudFile(cloudAddressBook);
+ }
+
+ /**
+ * Attempts to read a CloudAddressBook from the given cloudDataFilePath,
+ * then initializes a cloud file with the read cloudAddressBook
+ *
+ * Initializes an empty cloud file with addressBookName if read from cloudDataFilePath fails
+ *
+ * @param cloudDataFilePath
+ * @param addressBookName name of empty address book if read fails
+ */
+ private void initializeCloudFile(String cloudDataFilePath, String addressBookName) {
+ CloudAddressBook cloudAddressBook;
+ try {
+ cloudAddressBook = fileHandler.readCloudAddressBookFromExternalFile(cloudDataFilePath);
+ initializeCloudFile(cloudAddressBook);
+ } catch (DataConversionException e) {
+ logger.fatal("Error reading from cloud data file: {}", cloudDataFilePath);
+ assert false : "Error initializing cloud file: data conversion error during file reading";
+ } catch (FileNotFoundException e) {
+ logger.warn("Invalid cloud data file path provided: {}. Using empty address book for cloud", cloudDataFilePath);
+ initializeCloudFile(new CloudAddressBook(addressBookName));
+ }
+ }
+
+ /**
+ * Initializes a cloud file with the cloudAddressBook
+ *
+ * @param cloudAddressBook
+ */
+ private void initializeCloudFile(CloudAddressBook cloudAddressBook) {
+ try {
+ this.addressBookName = cloudAddressBook.getName();
+ fileHandler.initializeAddressBook(cloudAddressBook.getName());
+ fileHandler.writeCloudAddressBook(cloudAddressBook);
+ } catch (FileNotFoundException e) {
+ logger.fatal("Cloud file cannot be found for: {}", cloudAddressBook.getName());
+ assert false : "Error initializing cloud file: cloud file cannot be found.";
+ } catch (DataConversionException | IOException e) {
+ logger.fatal("Error initializing cloud file for: {}", cloudAddressBook.getName());
+ assert false : "Error initializing cloud file: data conversion error";
+ }
+ }
+
+ public void start(Stage stage) {
+ VBox consoleBox = getConsoleBox();
+
+ addressBookField = getAddressBookNameField(addressBookName);
+ failSyncUpsCheckBox = getCheckBox(FAIL_SYNC_UP_CHECKBOX_TEXT);
+ failSyncDownsCheckBox = getCheckBox(FAIL_SYNC_DOWN_CHECKBOX_TEXT);
+ statusArea = getStatusArea();
+
+ Button delayButton = getButton(DELAY_BUTTON_TEXT, getIcon(DELAY_BUTTON_ICON_PATH), actionEvent -> shouldDelayNext.set(true));
+ Button failButton = getButton(FAIL_BUTTON_TEXT, getIcon(FAIL_BUTTON_ICON_PATH), actionEvent -> shouldFailNext.set(true));
+ Button simulatePersonAdditionButton = getButton(ADD_PERSON_TEXT, null, actionEvent -> addRandomPersonToAddressBookFile(addressBookField::getText));
+ Button simulatePersonModificationButton = getButton(MODIFY_PERSON_TEXT, null, actionEvent -> modifyRandomPersonInAddressBookFile(addressBookField::getText));
+ Button simulatePersonDeletionButton = getButton(DELETE_PERSON_TEXT, null, actionEvent -> deleteRandomPersonInAddressBookFile(addressBookField::getText));
+ Button simulateTagAdditionButton = getButton(ADD_TAG_TEXT, null, actionEvent -> addRandomTagToAddressBookFile(addressBookField::getText));
+ Button simulateTagModificationButton = getButton(MODIFY_TAG_TEXT, null, actionEvent -> modifyRandomTagInAddressBookFile(addressBookField::getText));
+ Button simulateTagDeletionButton = getButton(DELETE_TAG_TEXT, null, actionEvent -> deleteRandomTagInAddressBookFile(addressBookField::getText));
+
+ consoleBox.getChildren().addAll(failSyncUpsCheckBox, failSyncDownsCheckBox, delayButton, failButton,
+ addressBookField, simulatePersonAdditionButton,
+ simulatePersonModificationButton, simulatePersonDeletionButton,
+ simulateTagAdditionButton, simulateTagModificationButton,
+ simulateTagDeletionButton, statusArea);
+
+ Dialog dialog = getConsoleDialog(stage);
+ dialog.getDialogPane().getChildren().add(consoleBox);
+ dialog.show();
+
+ shouldDelayNext.addListener((observable, oldValue, newValue) -> {
+ changeBackgroundColourBasedOnValue(newValue, delayButton);
+ });
+ shouldFailNext.addListener((observable, oldValue, newValue) -> {
+ changeBackgroundColourBasedOnValue(newValue, failButton);
+ });
+ }
+
+ private void changeBackgroundColourBasedOnValue(boolean isPending, Node node) {
+ if (isPending) {
+ node.setStyle("-fx-background-color: TURQUOISE");
+ } else {
+ node.setStyle("");
+ }
+ }
+
+ private Dialog getConsoleDialog(Stage stage) {
+ Dialog dialog = new Dialog<>();
+ dialog.initOwner(stage);
+ dialog.initModality(Modality.NONE); // so that the dialog does not prevent interaction with the app
+ dialog.setTitle(CLOUD_MANIPULATOR_TITLE);
+ dialog.setX(stage.getX() + stage.getWidth());
+ dialog.setY(stage.getY());
+ dialog.getDialogPane().setPrefWidth(CONSOLE_WIDTH);
+ dialog.getDialogPane().setPrefHeight(CONSOLE_HEIGHT);
+ return dialog;
+ }
+
+ private CheckBox getCheckBox(String checkBoxText) {
+ CheckBox failSyncUpsCheckBox = new CheckBox();
+ failSyncUpsCheckBox.setText(checkBoxText);
+ failSyncUpsCheckBox.setMinWidth(CONSOLE_WIDTH);
+ return failSyncUpsCheckBox;
+ }
+
+ private VBox getConsoleBox() {
+ VBox consoleBox = new VBox();
+ consoleBox.setMinWidth(CONSOLE_WIDTH);
+ return consoleBox;
+ }
+
+ private TextField getAddressBookNameField(String startingText) {
+ TextField addressBookNameField = new TextField();
+ if (startingText != null) {
+ addressBookNameField.setText(startingText);
+ }
+ addressBookNameField.setPromptText(ADDRESS_BOOK_FIELD_PROMPT_TEXT);
+ addressBookNameField.setMinWidth(CONSOLE_WIDTH);
+ addressBookNameField.setTooltip(new Tooltip(ADDRESS_BOOK_FIELD_TOOLTIP_TEXT));
+ return addressBookNameField;
+ }
+
+ private TextArea getStatusArea() {
+ TextArea statusArea = new TextArea();
+ statusArea.setEditable(false);
+ statusArea.setMinWidth(CONSOLE_WIDTH);
+ statusArea.setMinHeight(200);
+ statusArea.setWrapText(true);
+ return statusArea;
+ }
+
+ private ImageView getIcon(String resourcePath) {
+ URL url = getClass().getResource(resourcePath);
+ ImageView imageView = new ImageView(new Image(url.toString()));
+ imageView.setFitHeight(50);
+ imageView.setFitWidth(50);
+ return imageView;
+ }
+
+ private Button getButton(String text, ImageView graphic, EventHandler actionEventHandler) {
+ Button button = new Button(text);
+ button.setMinWidth(CONSOLE_WIDTH);
+ button.setGraphic(graphic);
+ button.setOnAction(actionEventHandler);
+ return button;
+ }
+
+ /**
+ * Returns a random element from the list
+ * @param list non-empty list
+ * @param