diff --git a/parse/src/main/java/com/parse/Parse.java b/parse/src/main/java/com/parse/Parse.java index d1c9a712c..d45f3df02 100644 --- a/parse/src/main/java/com/parse/Parse.java +++ b/parse/src/main/java/com/parse/Parse.java @@ -49,6 +49,7 @@ public class Parse { private static final Object MUTEX_CALLBACKS = new Object(); static ParseEventuallyQueue eventuallyQueue = null; private static boolean isLocalDatastoreEnabled; + private static boolean allowCustomObjectId = false; // endregion private static OfflineStore offlineStore; @@ -110,6 +111,14 @@ public static boolean isLocalDatastoreEnabled() { return isLocalDatastoreEnabled; } + /** + * @return {@code True} if {@link Configuration.Builder#allowCustomObjectId()} has been called, + * otherwise {@code false}. + */ + public static boolean isAllowCustomObjectId() { + return allowCustomObjectId; + } + /** * Authenticates this client as belonging to your application. This must be called before your * application can use the Parse library. The recommended way is to put a call to {@code @@ -140,6 +149,8 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) { // isLocalDataStoreEnabled() to perform additional behavior. isLocalDatastoreEnabled = configuration.localDataStoreEnabled; + allowCustomObjectId = configuration.allowCustomObjectId; + if (parsePlugins == null) { ParsePlugins.initialize(configuration.context, configuration); } else { @@ -271,6 +282,7 @@ public static void destroy() { ParsePlugins.reset(); setLocalDatastore(null); + allowCustomObjectId = false; } /** @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. */ @@ -573,6 +585,7 @@ public static final class Configuration { final String clientKey; final String server; final boolean localDataStoreEnabled; + final boolean allowCustomObjectId; final OkHttpClient.Builder clientBuilder; final int maxRetries; @@ -582,6 +595,7 @@ private Configuration(Builder builder) { this.clientKey = builder.clientKey; this.server = builder.server; this.localDataStoreEnabled = builder.localDataStoreEnabled; + this.allowCustomObjectId = builder.allowCustomObjectId; this.clientBuilder = builder.clientBuilder; this.maxRetries = builder.maxRetries; } @@ -593,6 +607,7 @@ public static final class Builder { private String clientKey; private String server; private boolean localDataStoreEnabled; + private boolean allowCustomObjectId; private OkHttpClient.Builder clientBuilder; private int maxRetries = DEFAULT_MAX_RETRIES; @@ -657,6 +672,20 @@ private Builder setLocalDatastoreEnabled(boolean enabled) { return this; } + /** + * Allow to set a custom objectId for ParseObjects. + * + * @return The same builder, for easy chaining. + */ + public Builder allowCustomObjectId() { + return this.setAllowCustomObjectId(true); + } + + private Builder setAllowCustomObjectId(boolean enabled) { + allowCustomObjectId = enabled; + return this; + } + /** * Set the {@link okhttp3.OkHttpClient.Builder} to use when communicating with the Parse * REST API diff --git a/parse/src/main/java/com/parse/ParseObject.java b/parse/src/main/java/com/parse/ParseObject.java index b06b8269e..d1176d011 100644 --- a/parse/src/main/java/com/parse/ParseObject.java +++ b/parse/src/main/java/com/parse/ParseObject.java @@ -2250,6 +2250,10 @@ Task saveAsync(final String sessionToken, final Task toAwait) { return Task.forResult(null); } + if (Parse.isAllowCustomObjectId() && getObjectId() == null) { + return Task.forError(new ParseException(104, "ObjectId must not be null")); + } + final ParseOperationSet operations; synchronized (mutex) { updateBeforeSave(); @@ -2357,6 +2361,10 @@ public final Task saveEventually() { return Task.forResult(null); } + if (Parse.isAllowCustomObjectId() && getObjectId() == null) { + return Task.forError(new ParseException(104, "ObjectId must not be null")); + } + final ParseOperationSet operationSet; final ParseRESTCommand command; final Task runEventuallyTask; diff --git a/parse/src/main/java/com/parse/ParseRESTObjectCommand.java b/parse/src/main/java/com/parse/ParseRESTObjectCommand.java index 12c3f35fc..d1ad41519 100644 --- a/parse/src/main/java/com/parse/ParseRESTObjectCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTObjectCommand.java @@ -36,8 +36,18 @@ public static ParseRESTObjectCommand saveObjectCommand( return ParseRESTObjectCommand.createObjectCommand( state.className(), operations, sessionToken); } else { - return ParseRESTObjectCommand.updateObjectCommand( - state.objectId(), state.className(), operations, sessionToken); + if (Parse.isAllowCustomObjectId()) { + if (state.createdAt() == -1) { + return ParseRESTObjectCommand.createObjectCommand( + state.className(), operations, sessionToken); + } else { + return ParseRESTObjectCommand.updateObjectCommand( + state.objectId(), state.className(), operations, sessionToken); + } + } else { + return ParseRESTObjectCommand.updateObjectCommand( + state.objectId(), state.className(), operations, sessionToken); + } } } diff --git a/parse/src/test/java/com/parse/ParseClientConfigurationTest.java b/parse/src/test/java/com/parse/ParseClientConfigurationTest.java index d429bb7c3..b2a2fcd3d 100644 --- a/parse/src/test/java/com/parse/ParseClientConfigurationTest.java +++ b/parse/src/test/java/com/parse/ParseClientConfigurationTest.java @@ -25,12 +25,14 @@ public void testBuilder() { builder.applicationId("foo"); builder.clientKey("bar"); builder.enableLocalDataStore(); + builder.allowCustomObjectId(); Parse.Configuration configuration = builder.build(); assertNull(configuration.context); assertEquals(configuration.applicationId, "foo"); assertEquals(configuration.clientKey, "bar"); assertTrue(configuration.localDataStoreEnabled); + assertEquals(configuration.allowCustomObjectId, true); } @Test diff --git a/parse/src/test/java/com/parse/ParseObjectTest.java b/parse/src/test/java/com/parse/ParseObjectTest.java index 4f7d51600..94a86042c 100644 --- a/parse/src/test/java/com/parse/ParseObjectTest.java +++ b/parse/src/test/java/com/parse/ParseObjectTest.java @@ -8,10 +8,13 @@ */ package com.parse; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; @@ -46,7 +49,7 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) -public class ParseObjectTest { +public class ParseObjectTest extends ResetPluginsParseTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -80,16 +83,17 @@ private static TaskCompletionSource mockObjectControllerForDelete() { } @Before - public void setUp() { + public void setUp() throws Exception { + super.setUp(); ParseFieldOperations.registerDefaultDecoders(); // to test JSON / Parcel decoding } // region testRevert @After - public void tearDown() { - ParseCorePlugins.getInstance().reset(); - ParsePlugins.reset(); + public void tearDown() throws Exception { + super.tearDown(); + Parse.destroy(); } @Test @@ -159,6 +163,118 @@ public void testFromJsonWithLdsStackOverflow() throws JSONException { // endregion + @Test + public void testSaveCustomObjectIdMissing() { + // Mocked to let save work + mockCurrentUserController(); + + Parse.Configuration configuration = + new Parse.Configuration.Builder(RuntimeEnvironment.application) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) + .server("https://api.parse.com/1") + .enableLocalDataStore() + .allowCustomObjectId() + .build(); + ParsePlugins plugins = mock(ParsePlugins.class); + when(plugins.configuration()).thenReturn(configuration); + when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); + Parse.initialize(configuration, plugins); + + ParseObject object = new ParseObject("TestObject"); + try { + object.save(); + } catch (ParseException e) { + assertEquals(e.getCode(), 104); + assertThat(e.getMessage(), is("ObjectId must not be null")); + } + } + + @Test + public void testSaveCustomObjectIdNotMissing() { + // Mocked to let save work + mockCurrentUserController(); + + Parse.Configuration configuration = + new Parse.Configuration.Builder(RuntimeEnvironment.application) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) + .server("https://api.parse.com/1") + .enableLocalDataStore() + .allowCustomObjectId() + .build(); + ParsePlugins plugins = mock(ParsePlugins.class); + when(plugins.configuration()).thenReturn(configuration); + when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); + Parse.initialize(configuration, plugins); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("ABCDEF123456"); + + ParseException exception = null; + try { + object.save(); + } catch (ParseException e) { + exception = e; + } + assertNull(exception); + } + + @Test + public void testSaveEventuallyCustomObjectIdMissing() { + // Mocked to let save work + mockCurrentUserController(); + + Parse.Configuration configuration = + new Parse.Configuration.Builder(RuntimeEnvironment.application) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) + .server("https://api.parse.com/1") + .enableLocalDataStore() + .allowCustomObjectId() + .build(); + ParsePlugins plugins = ParseTestUtils.mockParsePlugins(configuration); + Parse.initialize(configuration, plugins); + + ParseObject object = new ParseObject("TestObject"); + object.saveEventually( + new SaveCallback() { + @Override + public void done(ParseException e) { + assertNotNull(e); + assertEquals(e.getCode(), 104); + assertThat(e.getMessage(), is("ObjectId must not be null")); + } + }); + + Parse.setLocalDatastore(null); + } + + @Test + public void testSaveEventuallyCustomObjectIdNotMissing() throws ParseException { + // Mocked to let save work + mockCurrentUserController(); + + Parse.Configuration configuration = + new Parse.Configuration.Builder(RuntimeEnvironment.application) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) + .server("https://api.parse.com/1") + .enableLocalDataStore() + .allowCustomObjectId() + .build(); + ParsePlugins plugins = ParseTestUtils.mockParsePlugins(configuration); + Parse.initialize(configuration, plugins); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("ABCDEF123456"); + object.saveEventually( + new SaveCallback() { + @Override + public void done(ParseException e) { + assertNull(e); + } + }); + + Parse.setLocalDatastore(null); + } + // region testGetter @Test diff --git a/parse/src/test/java/com/parse/ParseRESTCommandTest.java b/parse/src/test/java/com/parse/ParseRESTCommandTest.java index e0cb4f619..0d825db2e 100644 --- a/parse/src/test/java/com/parse/ParseRESTCommandTest.java +++ b/parse/src/test/java/com/parse/ParseRESTCommandTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Collections; import org.json.JSONArray; import org.json.JSONObject; import org.junit.After; @@ -38,6 +39,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.skyscreamer.jsonassert.JSONCompareMode; // For org.json @@ -488,7 +490,7 @@ public void testOnResponseCloseNetworkStreamWithNormalResponse() throws Exceptio } @Test - public void testOnResposneCloseNetworkStreamWithIOException() throws Exception { + public void testOnResponseCloseNetworkStreamWithIOException() throws Exception { // Mock response stream int statusCode = 200; InputStream mockResponseStream = mock(InputStream.class); @@ -515,4 +517,39 @@ public void testOnResposneCloseNetworkStreamWithIOException() throws Exception { assertEquals("Error", responseTask.getError().getMessage()); verify(mockResponseStream, times(1)).close(); } + + @Test + public void testSaveObjectCommandUpdate() { + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.objectId()).thenReturn("test_id"); + when(state.createdAt()).thenReturn(System.currentTimeMillis() / 1000L); + when(state.updatedAt()).thenReturn(System.currentTimeMillis() / 1000L); + when(state.keySet()).thenReturn(Collections.singleton("foo")); + when(state.get("foo")).thenReturn("bar"); + ParseObject parseObject = ParseObject.from(state); + + ParseRESTObjectCommand command = + ParseRESTObjectCommand.saveObjectCommand(parseObject.getState(), null, null); + assertEquals(command.method, ParseHttpRequest.Method.PUT); + + Parse.Configuration configuration = + new Parse.Configuration.Builder(RuntimeEnvironment.application) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) + .server("https://api.parse.com/1") + .enableLocalDataStore() + .allowCustomObjectId() + .build(); + ParsePlugins plugins = mock(ParsePlugins.class); + when(plugins.configuration()).thenReturn(configuration); + when(plugins.applicationContext()).thenReturn(RuntimeEnvironment.application); + Parse.initialize(configuration, plugins); + + command = ParseRESTObjectCommand.saveObjectCommand(parseObject.getState(), null, null); + assertEquals(command.method, ParseHttpRequest.Method.PUT); + + ParseCorePlugins.getInstance().reset(); + ParsePlugins.reset(); + Parse.destroy(); + } }