diff --git a/Parse/src/main/java/com/parse/OfflineStore.java b/Parse/src/main/java/com/parse/OfflineStore.java index 233a784b5..2c193f1d2 100644 --- a/Parse/src/main/java/com/parse/OfflineStore.java +++ b/Parse/src/main/java/com/parse/OfflineStore.java @@ -1456,7 +1456,17 @@ public Integer then(Task> task) throws Exception { if (oldObjectId.equals(newObjectId)) { return; } - throw new RuntimeException("objectIds cannot be changed in offline mode."); + /** + * Special case for re-saving installation if it was deleted on the server + * @see ParseInstallation#saveAsync(String, Task) + */ + if (object instanceof ParseInstallation + && newObjectId == null) { + classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), oldObjectId)); + return; + } else { + throw new RuntimeException("objectIds cannot be changed in offline mode."); + } } String className = object.getClassName(); diff --git a/Parse/src/main/java/com/parse/ParseException.java b/Parse/src/main/java/com/parse/ParseException.java index d57b99cad..49148c754 100644 --- a/Parse/src/main/java/com/parse/ParseException.java +++ b/Parse/src/main/java/com/parse/ParseException.java @@ -134,6 +134,11 @@ public class ParseException extends Exception { */ public static final int INVALID_EMAIL_ADDRESS = 125; + /** + * Error code indicating that required field is missing. + */ + public static final int MISSING_REQUIRED_FIELD_ERROR = 135; + /** * Error code indicating that a unique field was given a value that is already taken. */ diff --git a/Parse/src/main/java/com/parse/ParseInstallation.java b/Parse/src/main/java/com/parse/ParseInstallation.java index 4fe460e20..5ad0be037 100644 --- a/Parse/src/main/java/com/parse/ParseInstallation.java +++ b/Parse/src/main/java/com/parse/ParseInstallation.java @@ -149,15 +149,16 @@ public Task then(Task task) throws Exception { @Override public Task then(Task task) throws Exception { // Retry the fetch as a save operation because this Installation was deleted on the server. - // Do not attempt to resave an object if LDS is enabled, since changing objectId is not allowed. - if(!Parse.isLocalDatastoreEnabled() - && task.getError() != null - && task.getError() instanceof ParseException - && ((ParseException) task.getError()).getCode() == ParseException.OBJECT_NOT_FOUND) { - synchronized (mutex) { - setState(new State.Builder(getState()).objectId(null).build()); - markAllFieldsDirty(); - return ParseInstallation.super.saveAsync(sessionToken, toAwait); + if (task.getError() != null + && task.getError() instanceof ParseException) { + int errCode = ((ParseException) task.getError()).getCode(); + if (errCode == ParseException.OBJECT_NOT_FOUND + || (errCode == ParseException.MISSING_REQUIRED_FIELD_ERROR && getObjectId() == null)) { + synchronized (mutex) { + setState(new State.Builder(getState()).objectId(null).build()); + markAllFieldsDirty(); + return ParseInstallation.super.saveAsync(sessionToken, toAwait); + } } } return task; diff --git a/Parse/src/test/java/com/parse/ParseInstallationTest.java b/Parse/src/test/java/com/parse/ParseInstallationTest.java index c46f80703..d2af55b5f 100644 --- a/Parse/src/test/java/com/parse/ParseInstallationTest.java +++ b/Parse/src/test/java/com/parse/ParseInstallationTest.java @@ -121,24 +121,48 @@ public void testInstallationObjectIdCannotBeChanged() throws Exception { } @Test - public void testSaveAsync() throws Exception { + public void testMissingRequiredFieldWhenSaveAsync() throws Exception { String sessionToken = "sessionToken"; Task toAwait = Task.forResult(null); - ParseCurrentInstallationController controller = - mock(ParseCurrentInstallationController.class); - ParseInstallation currentInstallation = new ParseInstallation(); - when(controller.getAsync()) - .thenReturn(Task.forResult(currentInstallation)); + ParseCurrentInstallationController controller = mockCurrentInstallationController(); + + ParseObjectController objController = mock(ParseObjectController.class); + // mock return task when Installation was deleted on the server + Task taskError = Task.forError(new ParseException(ParseException.MISSING_REQUIRED_FIELD_ERROR, "")); + // mock return task when Installation was re-saved to the server + Task task = Task.forResult(null); + when(objController.saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class))) + .thenReturn(taskError) + .thenReturn(task); ParseCorePlugins.getInstance() - .registerCurrentInstallationController(controller); - ParseObject.State state = new ParseObject.State.Builder("_Installation") - .put("deviceToken", "deviceToken") - .build(); + .registerObjectController(objController); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); assertNotNull(installation); - installation.setState(state); + installation.put("key", "value"); + installation.saveAsync(sessionToken, toAwait); + verify(controller).getAsync(); + verify(objController, times(2)).saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + eq(sessionToken), + any(ParseDecoder.class)); + } + @Test + public void testObjectNotFoundWhenSaveAsync() throws Exception { + OfflineStore lds = new OfflineStore(RuntimeEnvironment.application); + Parse.setLocalDatastore(lds); + + String sessionToken = "sessionToken"; + Task toAwait = Task.forResult(null); + + ParseCurrentInstallationController controller = mockCurrentInstallationController(); ParseObjectController objController = mock(ParseObjectController.class); // mock return task when Installation was deleted on the server Task taskError = Task.forError(new ParseException(ParseException.OBJECT_NOT_FOUND, "")); @@ -154,6 +178,14 @@ public void testSaveAsync() throws Exception { ParseCorePlugins.getInstance() .registerObjectController(objController); + ParseObject.State state = new ParseObject.State.Builder("_Installation") + .objectId("oldId") + .put("deviceToken", "deviceToken") + .build(); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + assertNotNull(installation); + installation.setState(state); + installation.put("key", "value"); installation.saveAsync(sessionToken, toAwait); verify(controller).getAsync(); @@ -355,4 +387,15 @@ private static void mocksForUpdateBeforeSave() { when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); ParsePlugins.set(plugins); } + + private ParseCurrentInstallationController mockCurrentInstallationController() { + ParseCurrentInstallationController controller = + mock(ParseCurrentInstallationController.class); + ParseInstallation currentInstallation = new ParseInstallation(); + when(controller.getAsync()) + .thenReturn(Task.forResult(currentInstallation)); + ParseCorePlugins.getInstance() + .registerCurrentInstallationController(controller); + return controller; + } }